452 lines
16 KiB
JavaScript
452 lines
16 KiB
JavaScript
/* Processes page — sovereign://processes
|
|
*
|
|
* Tabs: Processes | Tabs
|
|
*
|
|
* - Processes: /proc-level view of all sovereign_browser PIDs (main,
|
|
* WebKit renderers with their hosted tabs, WebKit network/gpu, Tor,
|
|
* FIPS). Polled from sovereign://processes/list every 1s. Sortable
|
|
* columns. Renderer rows expand to show hosted tabs.
|
|
* - Tabs: per-tab probe view (cpu_busy_percent, fps, timers, net, heap,
|
|
* DOM, WPID). Polled from sovereign://processes/tabs every 1s.
|
|
* Click a row to load the drill-down (sovereign://processes/tab_probe).
|
|
*
|
|
* Uses XMLHttpRequest via sovereignGet() because WebKit's custom scheme
|
|
* handler does not reliably support fetch() for sovereign:// URLs.
|
|
*/
|
|
|
|
function sovereignGet(url) {
|
|
return new Promise(function (resolve, reject) {
|
|
var x = new XMLHttpRequest();
|
|
x.open('GET', url, true);
|
|
x.onreadystatechange = function () {
|
|
if (x.readyState !== 4) return;
|
|
try { resolve(JSON.parse(x.responseText)); }
|
|
catch (e) { reject(e); }
|
|
};
|
|
x.onerror = function () { reject(new Error('XHR error')); };
|
|
x.send();
|
|
});
|
|
}
|
|
|
|
function esc(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s == null ? '' : String(s);
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function showMsg(text, isErr) {
|
|
var el = document.getElementById('status-msg');
|
|
el.textContent = text || '';
|
|
el.className = 'status show ' + (isErr ? 'err' : 'ok');
|
|
if (!text) el.className = 'status';
|
|
}
|
|
|
|
/* ── Formatting helpers ─────────────────────────────────────────── */
|
|
|
|
function fmtKb(kb) {
|
|
if (kb == null || kb < 0) return '—';
|
|
if (kb >= 1024 * 1024) return (kb / (1024 * 1024)).toFixed(1) + 'G';
|
|
if (kb >= 1024) return (kb / 1024).toFixed(0) + 'M';
|
|
return kb + 'K';
|
|
}
|
|
|
|
function fmtUptime(sec) {
|
|
if (sec == null || sec < 0) return '—';
|
|
var d = Math.floor(sec / 86400);
|
|
var h = Math.floor((sec % 86400) / 3600);
|
|
var m = Math.floor((sec % 3600) / 60);
|
|
var s = Math.floor(sec % 60);
|
|
if (d > 0) return d + 'd' + h + 'h';
|
|
if (h > 0) return h + 'h' + m + 'm';
|
|
if (m > 0) return m + 'm' + s + 's';
|
|
return s + 's';
|
|
}
|
|
|
|
function heatClass(pct) {
|
|
if (pct == null) return '';
|
|
if (pct >= 90) return 'hot';
|
|
return '';
|
|
}
|
|
|
|
function heatHtml(pct) {
|
|
if (pct == null) return '<span class="heat">—</span>';
|
|
return '<span class="heat ' + heatClass(pct) + '">' +
|
|
pct.toFixed(1) + '</span>';
|
|
}
|
|
|
|
/* ── Tab switching ──────────────────────────────────────────────── */
|
|
|
|
function switchTab(tabName) {
|
|
document.querySelectorAll('.tab-btn').forEach(function (btn) {
|
|
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
|
});
|
|
document.querySelectorAll('.tab-panel').forEach(function (panel) {
|
|
panel.classList.toggle('active', panel.id === 'tab-' + tabName);
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () { switchTab(btn.dataset.tab); });
|
|
});
|
|
|
|
/* ── Processes tab ──────────────────────────────────────────────── */
|
|
|
|
var gProcSortKey = 'cpu_percent';
|
|
var gProcSortDir = 'desc';
|
|
var gProcData = [];
|
|
var gExpandedRenderers = {}; // pid -> bool
|
|
|
|
function setProcSort(key) {
|
|
if (gProcSortKey === key) {
|
|
gProcSortDir = gProcSortDir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
gProcSortKey = key;
|
|
gProcSortDir = (key === 'name' || key === 'state' || key === 'ownership')
|
|
? 'asc' : 'desc';
|
|
}
|
|
renderProcTable();
|
|
}
|
|
|
|
document.querySelectorAll('#proc-table th[data-sort]').forEach(function (th) {
|
|
th.addEventListener('click', function () { setProcSort(th.dataset.sort); });
|
|
});
|
|
|
|
function procSorter(a, b) {
|
|
var ka = a[gProcSortKey];
|
|
var kb = b[gProcSortKey];
|
|
var cmp;
|
|
if (typeof ka === 'number' || typeof kb === 'number') {
|
|
cmp = (ka || 0) - (kb || 0);
|
|
} else {
|
|
cmp = String(ka || '').localeCompare(String(kb || ''));
|
|
}
|
|
return gProcSortDir === 'asc' ? cmp : -cmp;
|
|
}
|
|
|
|
function renderProcTable() {
|
|
var body = document.getElementById('proc-body');
|
|
body.innerHTML = '';
|
|
var sorted = gProcData.slice().sort(procSorter);
|
|
|
|
// Update sort indicators.
|
|
document.querySelectorAll('#proc-table th[data-sort]').forEach(function (th) {
|
|
th.classList.remove('sorted-asc', 'sorted-desc');
|
|
if (th.dataset.sort === gProcSortKey) {
|
|
th.classList.add(gProcSortDir === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
}
|
|
});
|
|
|
|
sorted.forEach(function (p) {
|
|
var tr = document.createElement('tr');
|
|
var isRenderer = p.ownership === 'webkit-renderer';
|
|
tr.className = isRenderer ? 'renderer-row' : '';
|
|
var tabsCount = (p.hosted_tabs || []).length;
|
|
var tabsCell;
|
|
if (isRenderer) {
|
|
tabsCell = String(tabsCount);
|
|
} else {
|
|
tabsCell = '—';
|
|
}
|
|
tr.innerHTML =
|
|
'<td>' + p.pid + '</td>' +
|
|
'<td class="title-cell">' + esc(p.name) + '</td>' +
|
|
'<td>' + heatHtml(p.cpu_percent) + '</td>' +
|
|
'<td>' + fmtKb(p.rss_kb) + '</td>' +
|
|
'<td>' + fmtKb(p.pss_kb) + '</td>' +
|
|
'<td>' + (p.threads || 0) + '</td>' +
|
|
'<td>' + fmtUptime(p.uptime_sec) + '</td>' +
|
|
'<td>' + esc(p.state) + '</td>' +
|
|
'<td>' + esc(p.ownership) +
|
|
(p.service_state ? ' (' + esc(p.service_state) + ')' : '') + '</td>' +
|
|
'<td>' + tabsCell + '</td>';
|
|
if (isRenderer) {
|
|
(function (pid, hosted) {
|
|
tr.addEventListener('click', function () {
|
|
gExpandedRenderers[pid] = !gExpandedRenderers[pid];
|
|
renderProcTable();
|
|
});
|
|
})(p.pid, p.hosted_tabs);
|
|
}
|
|
body.appendChild(tr);
|
|
|
|
// Expanded hosted-tabs row.
|
|
if (isRenderer && gExpandedRenderers[p.pid] && tabsCount > 0) {
|
|
var sub = document.createElement('tr');
|
|
sub.className = 'hosted-tabs-row';
|
|
var html = '<td colspan="10">';
|
|
p.hosted_tabs.forEach(function (t) {
|
|
html += '<span class="ht-entry"><span class="ht-idx">#' +
|
|
t.index + '</span>' + esc(t.title || '(untitled)') +
|
|
' <span class="url-sub">' + esc(t.url) + '</span></span>';
|
|
});
|
|
html += '</td>';
|
|
sub.innerHTML = html;
|
|
body.appendChild(sub);
|
|
}
|
|
});
|
|
|
|
if (sorted.length === 0) {
|
|
body.innerHTML = '<tr><td colspan="10" class="empty">No processes.</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function fetchProcesses() {
|
|
try {
|
|
var data = await sovereignGet('sovereign://processes/list?_t=' + Date.now());
|
|
gProcData = Array.isArray(data) ? data : [];
|
|
renderProcTable();
|
|
} catch (e) {
|
|
showMsg('Failed to load processes: ' + e.message, true);
|
|
}
|
|
}
|
|
|
|
/* ── Tabs tab ───────────────────────────────────────────────────── */
|
|
|
|
var gTabsSortKey = 'cpu_busy_percent';
|
|
var gTabsSortDir = 'desc';
|
|
var gTabsData = [];
|
|
var gSelectedTab = -1;
|
|
|
|
function setTabsSort(key) {
|
|
if (gTabsSortKey === key) {
|
|
gTabsSortDir = gTabsSortDir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
gTabsSortKey = key;
|
|
gTabsSortDir = (key === 'title') ? 'asc' : 'desc';
|
|
}
|
|
renderTabsTable();
|
|
}
|
|
|
|
document.querySelectorAll('#tabs-table th[data-sort]').forEach(function (th) {
|
|
th.addEventListener('click', function () { setTabsSort(th.dataset.sort); });
|
|
});
|
|
|
|
function tabSorter(a, b) {
|
|
var ka = a[gTabsSortKey];
|
|
var kb = b[gTabsSortKey];
|
|
// For probe fields, fall back to -1 if probe is null.
|
|
if (gTabsSortKey !== 'index' && gTabsSortKey !== 'title' &&
|
|
gTabsSortKey !== 'webprocess_pid') {
|
|
ka = (a.probe && a.probe[gTabsSortKey] != null) ? a.probe[gTabsSortKey] : -1;
|
|
kb = (b.probe && b.probe[gTabsSortKey] != null) ? b.probe[gTabsSortKey] : -1;
|
|
}
|
|
var cmp;
|
|
if (typeof ka === 'number' || typeof kb === 'number') {
|
|
cmp = (ka || 0) - (kb || 0);
|
|
} else {
|
|
cmp = String(ka || '').localeCompare(String(kb || ''));
|
|
}
|
|
return gTabsSortDir === 'asc' ? cmp : -cmp;
|
|
}
|
|
|
|
function probeField(tab, key, fmt) {
|
|
if (!tab.probe) return '—';
|
|
var v = tab.probe[key];
|
|
if (v == null || v < 0) return '—';
|
|
return fmt ? fmt(v) : v;
|
|
}
|
|
|
|
function renderTabsTable() {
|
|
var body = document.getElementById('tabs-body');
|
|
body.innerHTML = '';
|
|
var sorted = gTabsData.slice().sort(tabSorter);
|
|
|
|
document.querySelectorAll('#tabs-table th[data-sort]').forEach(function (th) {
|
|
th.classList.remove('sorted-asc', 'sorted-desc');
|
|
if (th.dataset.sort === gTabsSortKey) {
|
|
th.classList.add(gTabsSortDir === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
}
|
|
});
|
|
|
|
sorted.forEach(function (t) {
|
|
var tr = document.createElement('tr');
|
|
tr.className = 'tab-row' + (t.index === gSelectedTab ? ' selected' : '');
|
|
var busy = (t.probe && t.probe.cpu_busy_percent != null)
|
|
? t.probe.cpu_busy_percent : null;
|
|
var titleClass = (t.index === gSelectedTab) ? '' : 'dim';
|
|
tr.innerHTML =
|
|
'<td>' + t.index + '</td>' +
|
|
'<td class="title-cell"><span class="' + titleClass + '">' +
|
|
esc(t.title || '(untitled)') + '</span>' +
|
|
(t.is_internal ? ' (internal)' : '') +
|
|
'<br><span class="url-sub">' + esc(t.url) + '</span></td>' +
|
|
'<td>' + (t.is_internal ? '<span class="heat">—</span>'
|
|
: heatHtml(busy)) + '</td>' +
|
|
'<td>' + probeField(t, 'fps') + '</td>' +
|
|
'<td>' + probeField(t, 'timer_count') + '</td>' +
|
|
'<td>' + probeField(t, 'net_in_flight') + '</td>' +
|
|
'<td>' + probeField(t, 'heap_used_mb', function (v) { return v + 'M'; }) + '</td>' +
|
|
'<td>' + probeField(t, 'dom_node_count') + '</td>' +
|
|
'<td>' + (t.webprocess_pid || 0) + '</td>';
|
|
(function (idx) {
|
|
tr.addEventListener('click', function () { openDrilldown(idx); });
|
|
})(t.index);
|
|
body.appendChild(tr);
|
|
});
|
|
|
|
if (sorted.length === 0) {
|
|
body.innerHTML = '<tr><td colspan="9" class="empty">No tabs.</td></tr>';
|
|
}
|
|
}
|
|
|
|
async function fetchTabs() {
|
|
try {
|
|
var data = await sovereignGet('sovereign://processes/tabs?_t=' + Date.now());
|
|
gTabsData = Array.isArray(data) ? data : [];
|
|
renderTabsTable();
|
|
// If a tab is selected, refresh its drill-down too.
|
|
if (gSelectedTab >= 0) refreshDrilldown();
|
|
} catch (e) {
|
|
showMsg('Failed to load tabs: ' + e.message, true);
|
|
}
|
|
}
|
|
|
|
/* ── Drill-down ─────────────────────────────────────────────────── */
|
|
|
|
async function openDrilldown(idx) {
|
|
gSelectedTab = idx;
|
|
renderTabsTable();
|
|
document.getElementById('drilldown').style.display = 'block';
|
|
await refreshDrilldown();
|
|
}
|
|
|
|
async function refreshDrilldown() {
|
|
if (gSelectedTab < 0) return;
|
|
try {
|
|
var d = await sovereignGet('sovereign://processes/tab_probe?index=' +
|
|
gSelectedTab);
|
|
renderDrilldown(d);
|
|
} catch (e) {
|
|
document.getElementById('dd-title').textContent = 'Tab #' + gSelectedTab;
|
|
document.getElementById('dd-summary').innerHTML =
|
|
'<p class="err-text">Failed to load drill-down: ' + esc(e.message) + '</p>';
|
|
}
|
|
}
|
|
|
|
function renderDrilldown(d) {
|
|
document.getElementById('dd-title').textContent =
|
|
'Tab #' + d.index + ' — ' + (d.title || '(untitled)');
|
|
|
|
var p = d.probe || {};
|
|
var rows = [
|
|
['URL', esc(d.url)],
|
|
['WebProcess PID', d.webprocess_pid || 0],
|
|
['CPU-busy %', p.cpu_busy_percent != null ? heatHtml(p.cpu_busy_percent) : '—'],
|
|
['FPS', p.fps != null ? p.fps : '—'],
|
|
['Active timers', p.timer_count != null ? p.timer_count : '—'],
|
|
['Net in-flight', p.net_in_flight != null ? p.net_in_flight : '—'],
|
|
['Net reqs (last sec)', p.net_requests_last_sec != null ? p.net_requests_last_sec : '—'],
|
|
['Heap used', p.heap_used_mb >= 0 ? p.heap_used_mb + 'M' : '—'],
|
|
['Event listeners', p.event_listener_count != null ? p.event_listener_count : '—'],
|
|
['Workers', p.worker_count != null ? p.worker_count : '—'],
|
|
['DOM nodes', p.dom_node_count >= 0 ? p.dom_node_count : '—']
|
|
];
|
|
var html = '';
|
|
rows.forEach(function (r) {
|
|
html += '<div class="info-row"><span>' + r[0] + '</span><span>' +
|
|
r[1] + '</span></div>';
|
|
});
|
|
document.getElementById('dd-summary').innerHTML = html;
|
|
|
|
// Long-task sources.
|
|
var ltBody = document.getElementById('dd-lt-sources-body');
|
|
ltBody.innerHTML = '';
|
|
(p.long_task_top_sources || []).forEach(function (s) {
|
|
ltBody.innerHTML += '<tr><td class="mono">' + esc(s.name) + '</td><td>' +
|
|
s.total_ms + '</td><td>' + s.count + '</td></tr>';
|
|
});
|
|
if (!p.long_task_top_sources || p.long_task_top_sources.length === 0) {
|
|
ltBody.innerHTML = '<tr><td colspan="3" class="empty">No long tasks.</td></tr>';
|
|
}
|
|
|
|
// Timers.
|
|
var tmBody = document.getElementById('dd-timers-body');
|
|
tmBody.innerHTML = '';
|
|
(p.timer_top || []).forEach(function (s) {
|
|
tmBody.innerHTML += '<tr><td class="mono">' + esc(s.code) + '</td><td>' +
|
|
s.interval_ms + '</td><td>' + s.count + '</td></tr>';
|
|
});
|
|
if (!p.timer_top || p.timer_top.length === 0) {
|
|
tmBody.innerHTML = '<tr><td colspan="3" class="empty">No active timers.</td></tr>';
|
|
}
|
|
|
|
// Network endpoints.
|
|
var netBody = document.getElementById('dd-net-body');
|
|
netBody.innerHTML = '';
|
|
(p.net_top_endpoints || []).forEach(function (s) {
|
|
netBody.innerHTML += '<tr><td class="mono">' + esc(s.url) + '</td><td>' +
|
|
s.count + '</td></tr>';
|
|
});
|
|
if (!p.net_top_endpoints || p.net_top_endpoints.length === 0) {
|
|
netBody.innerHTML = '<tr><td colspan="2" class="empty">No network requests.</td></tr>';
|
|
}
|
|
|
|
// Timeline.
|
|
var tl = document.getElementById('dd-timeline');
|
|
tl.innerHTML = '';
|
|
var timeline = p.long_task_timeline || [];
|
|
if (timeline.length === 0) {
|
|
tl.innerHTML = '<div class="tl-empty">No long tasks in the last 30s.</div>';
|
|
} else {
|
|
var maxDur = 1;
|
|
timeline.forEach(function (r) { if (r.duration > maxDur) maxDur = r.duration; });
|
|
timeline.forEach(function (r) {
|
|
var h = Math.max(2, Math.round((r.duration / maxDur) * 72));
|
|
var bar = document.createElement('span');
|
|
bar.className = 'tl-bar';
|
|
bar.style.height = h + 'px';
|
|
bar.title = r.duration + 'ms — ' + (r.name || 'unknown');
|
|
tl.appendChild(bar);
|
|
});
|
|
}
|
|
}
|
|
|
|
/* ── Drill-down actions ─────────────────────────────────────────── */
|
|
|
|
document.getElementById('dd-reload').addEventListener('click', function () {
|
|
if (gSelectedTab < 0) return;
|
|
// Trigger reload via the bridge — there's no dedicated endpoint, so
|
|
// we use the agent eval path indirectly. Simplest: navigate the tab
|
|
// by opening its URL again via sovereign://processes/tab_action.
|
|
sovereignGet('sovereign://processes/tab_action?index=' + gSelectedTab +
|
|
'&action=reload').then(function () { refreshDrilldown(); });
|
|
});
|
|
|
|
document.getElementById('dd-suspend').addEventListener('click', function () {
|
|
if (gSelectedTab < 0) return;
|
|
sovereignGet('sovereign://processes/tab_action?index=' + gSelectedTab +
|
|
'&action=suspend').then(function () { refreshDrilldown(); });
|
|
});
|
|
|
|
document.getElementById('dd-close').addEventListener('click', function () {
|
|
if (gSelectedTab < 0) return;
|
|
if (!confirm('Close tab #' + gSelectedTab + '?')) return;
|
|
sovereignGet('sovereign://processes/tab_action?index=' + gSelectedTab +
|
|
'&action=close').then(function () {
|
|
document.getElementById('drilldown').style.display = 'none';
|
|
gSelectedTab = -1;
|
|
fetchTabs();
|
|
});
|
|
});
|
|
|
|
document.getElementById('dd-close-panel').addEventListener('click', function () {
|
|
document.getElementById('drilldown').style.display = 'none';
|
|
gSelectedTab = -1;
|
|
renderTabsTable();
|
|
});
|
|
|
|
/* ── Refresh button + polling ───────────────────────────────────── */
|
|
|
|
document.getElementById('refresh-btn').addEventListener('click', function () {
|
|
fetchProcesses();
|
|
fetchTabs();
|
|
});
|
|
|
|
function poll() {
|
|
fetchProcesses();
|
|
fetchTabs();
|
|
}
|
|
|
|
poll();
|
|
setInterval(poll, 1000);
|