From f001986acb7e8ab95f1ad10fa59b395689c6764e Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Mon, 18 May 2026 19:02:25 +0000 Subject: [PATCH] processes subsystem --- index.html | 14 - kernel_ai/services/processes_runtime.py | 227 ++++++++++++- static/js/filesystem-map.js | 205 ++++++++---- static/js/memory-belt.js | 402 +++++++++++++++++++++++- static/js/proc-3d-visualization.js | 297 ----------------- static/js/processes-belt.js | 386 ++++++++++++++++++++++- 6 files changed, 1142 insertions(+), 389 deletions(-) delete mode 100644 static/js/proc-3d-visualization.js diff --git a/index.html b/index.html index a32b6bd..540fa6c 100755 --- a/index.html +++ b/index.html @@ -76,11 +76,6 @@

Linux Kernel Ring 0 Visualization

- - -
@@ -104,23 +99,14 @@

Linux Kernel Ring 0 Visualization

- - diff --git a/kernel_ai/services/processes_runtime.py b/kernel_ai/services/processes_runtime.py index 0d66905..e457516 100644 --- a/kernel_ai/services/processes_runtime.py +++ b/kernel_ai/services/processes_runtime.py @@ -124,6 +124,203 @@ def _parse_meminfo_kb(): return out +def _parse_vmstat_selected(): + keys = { + "pgscan_kswapd", + "pgscan_direct", + "pgsteal_kswapd", + "pgsteal_direct", + "pgfault", + "pgmajfault", + "compact_stall", + "oom_kill", + } + out = {} + try: + with open("/proc/vmstat", "r", encoding="utf-8", errors="ignore") as f: + for line in f: + parts = line.split() + if len(parts) != 2: + continue + key = parts[0].strip() + if key not in keys: + continue + try: + out[key] = int(parts[1]) + except ValueError: + continue + except OSError as exc: + log_event( + logger, + "DEBUG", + "Failed to read /proc/vmstat", + event_dataset="kernel_ai.app", + component="services.processes_runtime", + operation="_parse_vmstat_selected", + event_data={"error": str(exc)}, + ) + return out + + +def _parse_memory_psi(): + result = { + "some_avg10": 0.0, + "some_avg60": 0.0, + "some_avg300": 0.0, + "full_avg10": 0.0, + "full_avg60": 0.0, + "full_avg300": 0.0, + } + try: + with open("/proc/pressure/memory", "r", encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split() + scope = parts[0] + vals = {} + for token in parts[1:]: + if "=" not in token: + continue + k, v = token.split("=", 1) + try: + vals[k] = float(v) + except ValueError: + continue + if scope == "some": + result["some_avg10"] = float(vals.get("avg10", 0.0)) + result["some_avg60"] = float(vals.get("avg60", 0.0)) + result["some_avg300"] = float(vals.get("avg300", 0.0)) + elif scope == "full": + result["full_avg10"] = float(vals.get("avg10", 0.0)) + result["full_avg60"] = float(vals.get("avg60", 0.0)) + result["full_avg300"] = float(vals.get("avg300", 0.0)) + except OSError: + return result + return result + + +def _read_proc_status_fields(pid: int): + out = {} + try: + with open(f"/proc/{int(pid)}/status", "r", encoding="utf-8", errors="ignore") as f: + for line in f: + if ":" not in line: + continue + key, raw = line.split(":", 1) + out[key.strip()] = raw.strip() + except OSError: + return out + return out + + +def _status_kb(status_map: dict, key: str): + raw = str(status_map.get(key) or "").strip() + if not raw: + return 0 + parts = raw.split() + if not parts: + return 0 + try: + return int(parts[0]) + except ValueError: + return 0 + + +def _read_proc_faults(pid: int): + try: + with open(f"/proc/{int(pid)}/stat", "r", encoding="utf-8", errors="ignore") as f: + raw = f.read().strip() + except OSError: + return {"minflt": 0, "majflt": 0} + if not raw: + return {"minflt": 0, "majflt": 0} + try: + tail = raw.split(") ", 1)[1] + fields = tail.split() + # In tail (after comm), minflt index=7, majflt index=9. + minflt = int(fields[7]) if len(fields) > 7 else 0 + majflt = int(fields[9]) if len(fields) > 9 else 0 + return {"minflt": minflt, "majflt": majflt} + except (IndexError, ValueError): + return {"minflt": 0, "majflt": 0} + + +def _build_memory_process_pressure(syscall_nodes, meminfo_kb): + mt_kb = max(1, int((meminfo_kb or {}).get("MemTotal") or 0)) + psi = _parse_memory_psi() + vmstat = _parse_vmstat_selected() + psi_factor = 1.0 + min(2.0, float(psi.get("some_avg10", 0.0)) / 20.0 + float(psi.get("full_avg10", 0.0)) / 12.0) + rows = [] + workers = [] + + for node in syscall_nodes: + pid = int(node.get("pid") or 0) + if pid <= 0: + continue + name = str(node.get("name") or "unknown") + status_map = _read_proc_status_fields(pid) + rss_kb = _status_kb(status_map, "VmRSS") + if rss_kb <= 0: + rss_kb = max(0, int(int(node.get("rss_bytes") or 0) / 1024)) + anon_kb = _status_kb(status_map, "RssAnon") + file_kb = _status_kb(status_map, "RssFile") + swap_kb = _status_kb(status_map, "VmSwap") + faults = _read_proc_faults(pid) + minflt = int(faults.get("minflt") or 0) + majflt = int(faults.get("majflt") or 0) + rss_share = rss_kb / float(mt_kb) + swap_share = swap_kb / float(mt_kb) + mem_percent = float(node.get("memory_percent") or 0.0) + syscall_pressure = float(node.get("syscall_pressure") or 0.0) + score = ( + rss_share * 62.0 + + swap_share * 160.0 + + min(mem_percent, 100.0) * 0.33 + + min(syscall_pressure, 100.0) * 0.22 + + min(100.0, majflt * 0.0009) + ) * psi_factor + score = max(0.0, min(100.0, score)) + + role = "userspace" + low_name = name.lower() + if low_name.startswith("kswapd") or low_name.startswith("kcompactd") or low_name in {"oom_reaper", "ksmd"}: + role = "kernel_worker" + elif "systemd-oomd" in low_name: + role = "oomd" + + row = { + "pid": pid, + "name": name, + "role": role, + "rss_mb": round(rss_kb / 1024.0, 2), + "anon_mb": round(anon_kb / 1024.0, 2), + "file_mb": round(file_kb / 1024.0, 2), + "swap_mb": round(swap_kb / 1024.0, 2), + "memory_percent": round(mem_percent, 2), + "syscall_pressure": round(syscall_pressure, 2), + "minflt": minflt, + "majflt": majflt, + "pressure_score": round(score, 2), + } + rows.append(row) + if role != "userspace": + workers.append(row) + + rows.sort(key=lambda x: (float(x.get("pressure_score", 0.0)), float(x.get("rss_mb", 0.0))), reverse=True) + workers.sort(key=lambda x: float(x.get("pressure_score", 0.0)), reverse=True) + return ( + rows[:18], + workers[:8], + { + "psi_memory": psi, + "vmstat": vmstat, + "psi_factor": round(psi_factor, 4), + }, + ) + + def _memory_strip_blocks(kind, kb_k, mem_total_kb, n_blocks, seed0): mem_total_kb = max(1, int(mem_total_kb)) kb_k = max(0, int(kb_k)) @@ -313,6 +510,7 @@ def collect_processes_realtime(): "name": name, "user": user, "fd_count": fd_count, + "num_threads": threads, "syscall_pressure": syscall_pressure, "seccomp_mode": seccomp_mode, "memory_percent": round(mem, 2), @@ -409,6 +607,7 @@ def collect_processes_realtime(): "user": str(row.get("user") or ""), "syscall_pressure": int(row.get("syscall_pressure") or 0), "fd_count": int(row.get("fd_count") or 0), + "num_threads": int(row.get("num_threads") or 0), "seccomp_mode": str(row.get("seccomp_mode") or "unknown"), "connections": 0, "unique_peers": 0, @@ -427,6 +626,7 @@ def collect_processes_realtime(): "user": "", "syscall_pressure": 0, "fd_count": 0, + "num_threads": 0, "seccomp_mode": "unknown", "connections": 0, "unique_peers": 0, @@ -498,7 +698,18 @@ def _add_edge(src_pid, dst_pid, edge_type, weight): swap = psutil.swap_memory() meminfo_kb = _parse_meminfo_kb() strip_rows, mem_summary = _build_memory_visual_rows(meminfo_kb, syscall_nodes, vm, swap) - memory_visual = {"layout": "strips", "rows": strip_rows, "summary": mem_summary} + process_pressure, kernel_memory_workers, kernel_memory_state = _build_memory_process_pressure( + syscall_nodes, + meminfo_kb, + ) + memory_visual = { + "layout": "strips", + "rows": strip_rows, + "summary": mem_summary, + "process_pressure": process_pressure, + "kernel_memory_workers": kernel_memory_workers, + "kernel_memory_state": kernel_memory_state, + } except (psutil.Error, OSError, ValueError, TypeError) as exc: log_event( logger, @@ -514,6 +725,20 @@ def _add_edge(src_pid, dst_pid, edge_type, weight): "layout": "strips", "rows": [], "summary": {"total_mb": 0, "used_percent": 0.0, "available_mb": 0, "swap_percent": 0.0, "source": "error"}, + "process_pressure": [], + "kernel_memory_workers": [], + "kernel_memory_state": { + "psi_memory": { + "some_avg10": 0.0, + "some_avg60": 0.0, + "some_avg300": 0.0, + "full_avg10": 0.0, + "full_avg60": 0.0, + "full_avg300": 0.0, + }, + "vmstat": {}, + "psi_factor": 1.0, + }, } return { diff --git a/static/js/filesystem-map.js b/static/js/filesystem-map.js index daaba5b..e1988e9 100644 --- a/static/js/filesystem-map.js +++ b/static/js/filesystem-map.js @@ -25,10 +25,12 @@ class FilesystemMapVisualization { this.zoneHitAreas = []; this.selectedZoneId = null; this.hoveredZoneId = null; + this.radialHitModel = null; this.zoneSortMode = 'risk'; this.sortModeButton = null; - this.archMapMode = 'simple'; - this.archModeButton = null; + this.archMapMode = 'detailed'; + this.focusMode = 'soft'; + this.focusModeButton = null; } init(containerId = 'filesystem-map-container') { @@ -167,9 +169,9 @@ class FilesystemMapVisualization { this.sortModeButton = sortBtn; this.updateSortModeButtonState(); - const archBtn = document.createElement('button'); - archBtn.textContent = 'ARCH: SIMPLE'; - archBtn.style.cssText = ` + const focusBtn = document.createElement('button'); + focusBtn.textContent = 'FOCUS: SOFT'; + focusBtn.style.cssText = ` position: absolute; top: 72px; left: 136px; @@ -183,11 +185,11 @@ class FilesystemMapVisualization { cursor: pointer; z-index: 1001; `; - archBtn.onclick = () => this.cycleArchMapMode(); - this.container.appendChild(archBtn); - this.overlayNodes.push(archBtn); - this.archModeButton = archBtn; - this.updateArchMapModeButtonState(); + focusBtn.onclick = () => this.toggleFocusMode(); + this.container.appendChild(focusBtn); + this.overlayNodes.push(focusBtn); + this.focusModeButton = focusBtn; + this.updateFocusModeButtonState(); const info = document.createElement('div'); info.style.cssText = ` @@ -290,31 +292,37 @@ class FilesystemMapVisualization { ctx.closePath(); } - drawPackageStack(x, y, w, h, layers, accentColor, label, metaText, subMetaText, isSelected, isHovered) { + drawPackageStack(x, y, w, h, layers, accentColor, label, metaText, subMetaText, isSelected, isHovered, isMuted = false, focusMode = 'soft') { const ctx = this.ctx; + const hardFocus = focusMode === 'hard'; const stackLayers = Math.max(2, Math.min(4, layers)); for (let i = stackLayers - 1; i >= 0; i -= 1) { const dx = -i * 4; const dy = i * 3; this.drawRoundedRect(x + dx, y + dy, w, h, 5); - ctx.fillStyle = i === 0 ? 'rgba(14, 24, 36, 0.95)' : 'rgba(10, 16, 26, 0.85)'; + ctx.fillStyle = i === 0 + ? (isMuted ? (hardFocus ? 'rgba(12, 18, 28, 0.46)' : 'rgba(12, 18, 28, 0.64)') : 'rgba(14, 24, 36, 0.95)') + : (isMuted ? (hardFocus ? 'rgba(10, 16, 26, 0.3)' : 'rgba(10, 16, 26, 0.5)') : 'rgba(10, 16, 26, 0.85)'); ctx.fill(); const baseStroke = i === 0 ? accentColor : 'rgba(72, 94, 124, 0.35)'; - ctx.strokeStyle = isSelected ? 'rgba(149, 207, 255, 0.95)' : (isHovered ? 'rgba(122, 182, 245, 0.82)' : baseStroke); + ctx.strokeStyle = isMuted + ? (hardFocus ? 'rgba(82, 104, 132, 0.22)' : 'rgba(82, 104, 132, 0.38)') + : (isSelected ? 'rgba(149, 207, 255, 0.95)' : (isHovered ? 'rgba(122, 182, 245, 0.82)' : baseStroke)); ctx.lineWidth = isSelected ? 1.4 : (i === 0 ? 1.0 : 0.7); ctx.stroke(); } - ctx.fillStyle = isSelected ? '#d9ecff' : '#9fc0e8'; + ctx.fillStyle = isMuted ? (hardFocus ? 'rgba(132, 152, 178, 0.45)' : 'rgba(132, 152, 178, 0.62)') : (isSelected ? '#d9ecff' : '#9fc0e8'); + ctx.font = '10px "Share Tech Mono", monospace'; + ctx.fillText(label, x + 8, y + 14); + ctx.fillStyle = isMuted ? (hardFocus ? 'rgba(124, 140, 160, 0.4)' : 'rgba(124, 140, 160, 0.58)') : (isSelected ? 'rgba(198, 222, 250, 0.96)' : 'rgba(155, 176, 204, 0.9)'); ctx.font = '9px "Share Tech Mono", monospace'; - ctx.fillText(label, x + 8, y + 13); - ctx.fillStyle = isSelected ? 'rgba(190, 214, 241, 0.92)' : 'rgba(122, 141, 167, 0.85)'; - ctx.font = '8px "Share Tech Mono", monospace'; - ctx.fillText(metaText, x + 8, y + 24); + ctx.fillText(metaText, x + 8, y + 27); if (subMetaText) { - ctx.fillStyle = 'rgba(132, 156, 186, 0.76)'; - ctx.fillText(subMetaText, x + 8, y + 34); + ctx.fillStyle = isMuted ? (hardFocus ? 'rgba(120, 138, 162, 0.3)' : 'rgba(120, 138, 162, 0.46)') : 'rgba(132, 156, 186, 0.76)'; + ctx.font = '8px "Share Tech Mono", monospace'; + ctx.fillText(subMetaText, x + 8, y + 38); } - ctx.fillStyle = accentColor; + ctx.fillStyle = isMuted ? (hardFocus ? 'rgba(88, 104, 128, 0.34)' : 'rgba(88, 104, 128, 0.54)') : accentColor; ctx.fillRect(x + 2, y + h - 3, Math.max(8, w - 4), 2); } @@ -460,14 +468,15 @@ class FilesystemMapVisualization { } } - drawLiveBlockMatrix(x, y, w, h, blocks, zones, meta) { + drawLiveBlockMatrix(x, y, w, h, blocks, zones, meta, selectedZoneId = null, focusMode = 'soft') { const ctx = this.ctx; + const hardFocus = focusMode === 'hard'; const zoneMap = new Map((Array.isArray(zones) ? zones : []).map((z) => [String(z.id || ''), z])); const data = Array.isArray(blocks) ? blocks : []; if (!data.length) return; this.drawRoundedRect(x, y, w, h, 7); - ctx.fillStyle = 'rgba(8, 13, 21, 0.72)'; + ctx.fillStyle = 'rgba(8, 13, 21, 0.62)'; ctx.fill(); ctx.strokeStyle = 'rgba(122, 146, 178, 0.28)'; ctx.lineWidth = 0.85; @@ -486,6 +495,7 @@ class FilesystemMapVisualization { for (let i = 0; i < rows * cols; i += 1) { const b = data[i % data.length] || {}; + const zoneId = String(b.zone_id || ''); const zone = zoneMap.get(String(b.zone_id || '')) || {}; const zx = i % cols; const zy = Math.floor(i / cols); @@ -494,17 +504,19 @@ class FilesystemMapVisualization { const zoneAct = Math.max(0, Math.min(100, Number(zone.activity || 0))); const zInode = Math.max(0, Math.min(100, Number(zone.inode_pressure || 0))); const flicker = 0.75 + 0.25 * Math.sin(this.tick * 0.05 + zx * 0.17 + zy * 0.23); + const isFocused = !selectedZoneId || zoneId === String(selectedZoneId); + const focusAlphaMul = isFocused ? 1 : (hardFocus ? 0.26 : 0.5); if (b.state === 'writing') { const alpha = 0.46 + Math.min(0.5, (zoneAct / 130) + (writeHot > 90 ? 0.18 : 0)) * flicker; - ctx.fillStyle = `rgba(163, 238, 255, ${Math.min(0.95, alpha).toFixed(3)})`; + ctx.fillStyle = `rgba(163, 238, 255, ${Math.min(0.95, alpha * focusAlphaMul).toFixed(3)})`; } else if (b.state === 'used') { const alpha = 0.22 + Math.min(0.5, ((zoneAct + inodePressure * 0.35) / 220)) * flicker; ctx.fillStyle = zInode >= 78 - ? `rgba(255, 184, 132, ${Math.min(0.8, alpha + 0.08).toFixed(3)})` - : `rgba(96, 166, 220, ${Math.min(0.8, alpha).toFixed(3)})`; + ? `rgba(255, 184, 132, ${Math.min(0.8, (alpha + 0.08) * focusAlphaMul).toFixed(3)})` + : `rgba(96, 166, 220, ${Math.min(0.8, alpha * focusAlphaMul).toFixed(3)})`; } else { - ctx.fillStyle = `rgba(17, 30, 46, ${(0.24 + 0.08 * flicker).toFixed(3)})`; + ctx.fillStyle = `rgba(17, 30, 46, ${((0.24 + 0.08 * flicker) * focusAlphaMul).toFixed(3)})`; } const cw = Math.max(1.5, cellW - 1.15); @@ -585,18 +597,18 @@ class FilesystemMapVisualization { } } - cycleArchMapMode() { - this.archMapMode = this.archMapMode === 'detailed' ? 'simple' : 'detailed'; - this.updateArchMapModeButtonState(); + toggleFocusMode() { + this.focusMode = this.focusMode === 'hard' ? 'soft' : 'hard'; + this.updateFocusModeButtonState(); } - updateArchMapModeButtonState() { - if (!this.archModeButton) return; - const detailed = this.archMapMode === 'detailed'; - this.archModeButton.textContent = detailed ? 'ARCH: DETAILED' : 'ARCH: SIMPLE'; - this.archModeButton.style.background = detailed ? 'rgba(28, 45, 64, 0.92)' : 'rgba(12, 18, 28, 0.9)'; - this.archModeButton.style.borderColor = detailed ? 'rgba(127, 194, 255, 0.88)' : 'rgba(130, 148, 172, 0.35)'; - this.archModeButton.style.color = detailed ? '#e0f1ff' : '#9fb0c8'; + updateFocusModeButtonState() { + if (!this.focusModeButton) return; + const hard = this.focusMode === 'hard'; + this.focusModeButton.textContent = hard ? 'FOCUS: HARD' : 'FOCUS: SOFT'; + this.focusModeButton.style.background = hard ? 'rgba(68, 34, 44, 0.92)' : 'rgba(24, 42, 60, 0.9)'; + this.focusModeButton.style.borderColor = hard ? 'rgba(255, 158, 172, 0.86)' : 'rgba(132, 194, 255, 0.78)'; + this.focusModeButton.style.color = hard ? '#ffe3e7' : '#dff1ff'; } getZoneHitAt(x, y) { @@ -608,6 +620,24 @@ class FilesystemMapVisualization { return null; } + getRadialZoneAt(x, y) { + const model = this.radialHitModel; + if (!model) return null; + const dx = x - model.cx; + const dy = y - model.cy; + const dist = Math.hypot(dx, dy); + if (dist < model.innerR || dist > model.outerR) return null; + const rIdx = Math.floor((dist - model.innerR) / model.ringGap); + if (rIdx < 0 || rIdx >= model.rows) return null; + let a = Math.atan2(dy, dx); + if (a < 0) a += Math.PI * 2; + const cIdx = Math.floor(a / model.angleStep); + if (cIdx < 0 || cIdx >= model.cols) return null; + const zoneId = model.zoneByCell.get(`${rIdx}:${cIdx}`) || null; + if (!zoneId) return null; + return { zoneId }; + } + onCanvasClick(event) { if (!this.canvas) return; const rect = this.canvas.getBoundingClientRect(); @@ -618,6 +648,11 @@ class FilesystemMapVisualization { this.selectedZoneId = this.selectedZoneId === zoneHit.zoneId ? null : zoneHit.zoneId; return; } + const radialHit = this.getRadialZoneAt(x, y); + if (radialHit) { + this.selectedZoneId = this.selectedZoneId === radialHit.zoneId ? null : radialHit.zoneId; + return; + } if (!this.orbHitArea) return; const dx = x - this.orbHitArea.x; const dy = y - this.orbHitArea.y; @@ -633,9 +668,10 @@ class FilesystemMapVisualization { const x = event.clientX - rect.left; const y = event.clientY - rect.top; const zoneHit = this.getZoneHitAt(x, y); - this.hoveredZoneId = zoneHit ? zoneHit.zoneId : null; + const radialHit = zoneHit ? null : this.getRadialZoneAt(x, y); + this.hoveredZoneId = zoneHit ? zoneHit.zoneId : (radialHit ? radialHit.zoneId : null); const overOrb = this.orbHitArea && Math.hypot(x - this.orbHitArea.x, y - this.orbHitArea.y) <= this.orbHitArea.r * 1.25; - this.canvas.style.cursor = zoneHit || overOrb ? 'pointer' : 'default'; + this.canvas.style.cursor = zoneHit || radialHit || overOrb ? 'pointer' : 'default'; } drawScene() { @@ -655,7 +691,11 @@ class FilesystemMapVisualization { const blocks = this.telemetry.blocks; const zones = Array.isArray(this.telemetry.zones) ? this.telemetry.zones : []; const zoneMap = new Map(zones.map((z) => [String(z.id || ''), z])); + const selectedZoneId = this.selectedZoneId ? String(this.selectedZoneId) : null; + const hasZoneFocus = Boolean(selectedZoneId); + const hardFocus = this.focusMode === 'hard'; this.zoneHitAreas = []; + this.radialHitModel = null; const cx = Math.floor(w * 0.53); const cy = Math.floor(h * 0.63); @@ -664,6 +704,7 @@ class FilesystemMapVisualization { const ringGap = (outerR - innerR) / Math.max(1, rows); const angleStep = (Math.PI * 2) / Math.max(1, cols); const m = this.telemetry.meta || {}; + const zoneByCell = new Map(); const kpis = [ { @@ -700,7 +741,9 @@ class FilesystemMapVisualization { }); // Background circuit lines. - this.ctx.strokeStyle = 'rgba(150, 38, 64, 0.17)'; + this.ctx.strokeStyle = hasZoneFocus + ? (hardFocus ? 'rgba(120, 52, 84, 0.1)' : 'rgba(120, 52, 84, 0.14)') + : 'rgba(150, 38, 64, 0.17)'; this.ctx.lineWidth = 1; for (let i = 0; i < 9; i += 1) { const y = 120 + i * 44; @@ -713,8 +756,8 @@ class FilesystemMapVisualization { const matrixW = Math.min(760, Math.max(520, Math.floor(w * 0.54))); const matrixH = Math.min(320, Math.max(220, Math.floor(h * 0.28))); const matrixX = Math.max(24, Math.min(w - matrixW - 24, cx - Math.floor(matrixW * 0.52))); - const matrixY = Math.max(118, cy - outerR - Math.floor(matrixH * 0.45)); - this.drawLiveBlockMatrix(matrixX, matrixY, matrixW, matrixH, blocks, zones, m); + const matrixY = Math.max(126, cy - outerR - Math.floor(matrixH * 0.42)); + this.drawLiveBlockMatrix(matrixX, matrixY, matrixW, matrixH, blocks, zones, m, selectedZoneId, this.focusMode); // Main rings. for (let i = 0; i <= 7; i += 1) { @@ -730,25 +773,31 @@ class FilesystemMapVisualization { blocks.forEach((b) => { const rIdx = Number.isFinite(Number(b.r)) ? Number(b.r) : Math.floor(Number(b.i || 0) / cols); const cIdx = Number.isFinite(Number(b.c)) ? Number(b.c) : (Number(b.i || 0) % cols); + if (rIdx >= 0 && rIdx < rows && cIdx >= 0 && cIdx < cols) { + zoneByCell.set(`${rIdx}:${cIdx}`, String(b.zone_id || '')); + } const r0 = innerR + rIdx * ringGap; const r1 = r0 + Math.max(1.3, ringGap * 0.8); const a0 = cIdx * angleStep; const a1 = a0 + angleStep * 0.88; + const blockZoneId = String(b.zone_id || ''); + const isFocused = !hasZoneFocus || blockZoneId === selectedZoneId; + const focusAlphaMul = isFocused ? 1 : (hardFocus ? 0.22 : 0.42); const z = zoneMap.get(String(b.zone_id || '')) || {}; const zoneAct = Math.max(0, Math.min(100, Number(z.activity || 0))); const inodePressure = Math.max(0, Math.min(100, Number(z.inode_pressure || 0))); if (this.renderMode === 'write') { - if (b.state === 'writing') this.ctx.fillStyle = 'rgba(127, 232, 255, 0.92)'; - else if (b.state === 'used') this.ctx.fillStyle = `rgba(44, 132, 184, ${0.32 + Math.min(0.5, zoneAct / 120)})`; - else this.ctx.fillStyle = 'rgba(14, 28, 44, 0.48)'; + if (b.state === 'writing') this.ctx.fillStyle = `rgba(127, 232, 255, ${(0.92 * focusAlphaMul).toFixed(3)})`; + else if (b.state === 'used') this.ctx.fillStyle = `rgba(44, 132, 184, ${((0.32 + Math.min(0.5, zoneAct / 120)) * focusAlphaMul).toFixed(3)})`; + else this.ctx.fillStyle = `rgba(14, 28, 44, ${(0.48 * focusAlphaMul).toFixed(3)})`; } else if (this.renderMode === 'inode') { - if (b.state === 'free') this.ctx.fillStyle = 'rgba(12, 24, 38, 0.45)'; - else this.ctx.fillStyle = `rgba(127, 232, 255, ${0.18 + Math.min(0.72, inodePressure / 110)})`; + if (b.state === 'free') this.ctx.fillStyle = `rgba(12, 24, 38, ${(0.45 * focusAlphaMul).toFixed(3)})`; + else this.ctx.fillStyle = `rgba(127, 232, 255, ${((0.18 + Math.min(0.72, inodePressure / 110)) * focusAlphaMul).toFixed(3)})`; } else { - if (b.state === 'writing') this.ctx.fillStyle = 'rgba(127, 232, 255, 0.9)'; - else if (b.state === 'free') this.ctx.fillStyle = 'rgba(19, 38, 58, 0.55)'; - else this.ctx.fillStyle = 'rgba(44, 132, 184, 0.72)'; + if (b.state === 'writing') this.ctx.fillStyle = `rgba(127, 232, 255, ${(0.9 * focusAlphaMul).toFixed(3)})`; + else if (b.state === 'free') this.ctx.fillStyle = `rgba(19, 38, 58, ${(0.55 * focusAlphaMul).toFixed(3)})`; + else this.ctx.fillStyle = `rgba(44, 132, 184, ${(0.72 * focusAlphaMul).toFixed(3)})`; } this.ctx.beginPath(); @@ -757,6 +806,17 @@ class FilesystemMapVisualization { this.ctx.closePath(); this.ctx.fill(); }); + this.radialHitModel = { + cx, + cy, + innerR, + outerR, + ringGap, + angleStep, + rows, + cols, + zoneByCell, + }; // Central hole. this.ctx.beginPath(); @@ -790,14 +850,14 @@ class FilesystemMapVisualization { const leftItems = zoneItems.filter((_, i) => i % 2 === 0); const rightItems = zoneItems.filter((_, i) => i % 2 === 1); const cardW = 130; - const cardH = 42; + const cardH = 46; const leftX = Math.max(24, cx - outerR - 220); const rightX = Math.min(w - cardW - 24, cx + outerR + 110); const topY = Math.max(150, cy - outerR + 12); const drawZonePack = (list, xBase, isLeft) => { list.forEach((z, idx) => { - const y = topY + idx * 50; + const y = topY + idx * 54; const usedPercent = Number(z.used_percent || 0); const writingBlocks = Number(z.writing_blocks || 0); const inodePressure = Number(z.inode_pressure || 0); @@ -810,6 +870,7 @@ class FilesystemMapVisualization { const zoneId = String(z.id || z.name || `zone-${idx}`); const isSelected = this.selectedZoneId === zoneId; const isHovered = this.hoveredZoneId === zoneId; + const isMuted = hasZoneFocus && !isSelected; this.drawPackageStack( xBase, y, @@ -821,29 +882,51 @@ class FilesystemMapVisualization { `used ${usedPercent.toFixed(0)}% | inode ${inodePressure.toFixed(0)}%`, `wr ${writingBlocks} | act ${Number(z.activity || 0).toFixed(0)}`, isSelected, - isHovered + isHovered, + isMuted, + this.focusMode ); this.zoneHitAreas.push({ x: xBase, y, w: cardW, h: cardH, zoneId }); - // Connector to central disk. + // Connector to central disk with light bundling. const sx = isLeft ? xBase + cardW : xBase; const sy = y + cardH / 2; const tx = cx + (isLeft ? -outerR * 0.85 : outerR * 0.85); - const ty = cy - outerR * 0.65 + idx * 10; + const ty = cy - outerR * 0.62 + idx * 9; + const bundleX = cx + (isLeft ? -outerR * 1.05 : outerR * 1.05); + const bundleY = cy - outerR * 0.5 + idx * 8; this.ctx.beginPath(); this.ctx.moveTo(sx, sy); this.ctx.bezierCurveTo( sx + (isLeft ? 60 : -60), sy, - tx + (isLeft ? -40 : 40), + bundleX + (isLeft ? -22 : 22), + bundleY, + bundleX, + bundleY + ); + this.ctx.strokeStyle = isMuted + ? (hardFocus ? 'rgba(82, 104, 132, 0.2)' : 'rgba(102, 122, 148, 0.32)') + : (isSelected + ? 'rgba(146, 204, 255, 0.86)' + : (riskScore >= 80 ? 'rgba(226, 92, 106, 0.44)' : 'rgba(169, 52, 80, 0.26)')); + this.ctx.lineWidth = isSelected ? 1.5 : (isMuted ? (hardFocus ? 0.8 : 0.95) : 1); + this.ctx.stroke(); + + this.ctx.beginPath(); + this.ctx.moveTo(bundleX, bundleY); + this.ctx.bezierCurveTo( + bundleX + (isLeft ? 24 : -24), + bundleY, + tx + (isLeft ? -34 : 34), ty, tx, ty ); - this.ctx.strokeStyle = isSelected - ? 'rgba(146, 204, 255, 0.86)' - : (riskScore >= 80 ? 'rgba(226, 92, 106, 0.44)' : 'rgba(169, 52, 80, 0.26)'); - this.ctx.lineWidth = isSelected ? 1.5 : 1; + this.ctx.strokeStyle = isMuted + ? (hardFocus ? 'rgba(82, 104, 132, 0.22)' : 'rgba(102, 122, 148, 0.35)') + : (isSelected ? 'rgba(146, 204, 255, 0.86)' : 'rgba(124, 156, 196, 0.34)'); + this.ctx.lineWidth = isSelected ? 1.3 : (isMuted ? (hardFocus ? 0.85 : 1) : 0.95); this.ctx.stroke(); }); }; @@ -869,7 +952,7 @@ class FilesystemMapVisualization { const archW = Math.min(620, Math.max(470, Math.floor(w * 0.44))); const archH = 228; const archX = Math.max(24, Math.min(w - archW - 24, rightX - 120)); - const archY = 96; + const archY = 108; this.drawFsArchitectureMap(archX, archY, archW, archH, m, zones); const selectedZone = zones.find((z) => String(z.id || z.name || '') === String(this.selectedZoneId || '')); diff --git a/static/js/memory-belt.js b/static/js/memory-belt.js index fcd90c2..2c82995 100644 --- a/static/js/memory-belt.js +++ b/static/js/memory-belt.js @@ -18,7 +18,14 @@ class MemorySubsystemVisualization { this.memoryStripHits = []; this.memoryMapHit = null; this.memoryHoverCell = null; + this.memoryFabricHits = []; + this.memorySelectedCell = null; + this.lastMouseX = 0; + this.lastMouseY = 0; this.mouseMoveHandler = null; + this.clickHandler = null; + this.viewMode = 'fabric'; + this.viewModeButton = null; } init(containerId = 'memory-belt-container') { @@ -39,7 +46,9 @@ class MemorySubsystemVisualization { this.container.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); this.mouseMoveHandler = (event) => this.onMouseMove(event); + this.clickHandler = (event) => this.onCanvasClick(event); this.canvas.addEventListener('mousemove', this.mouseMoveHandler); + this.canvas.addEventListener('click', this.clickHandler); this.onResize(); window.addEventListener('resize', () => this.onResize()); @@ -53,16 +62,53 @@ class MemorySubsystemVisualization { `; this.exitButton.onclick = () => window.location.assign('/'); this.container.appendChild(this.exitButton); + + const modeBtn = document.createElement('button'); + modeBtn.style.cssText = ` + position:absolute;top:18px;left:18px;padding:8px 12px;z-index:10021; + background: rgba(7, 10, 16, 0.92); border:1px solid rgba(178,190,212,0.45); + color:#d5dce8; font-family:'Share Tech Mono', monospace; font-size:11px; cursor:pointer; + box-shadow: 0 0 14px rgba(150,175,220,0.18); + `; + modeBtn.onclick = () => this.toggleViewMode(); + this.container.appendChild(modeBtn); + this.viewModeButton = modeBtn; + this.updateViewModeButton(); return true; } + toggleViewMode() { + this.viewMode = this.viewMode === 'fabric' ? 'strips' : 'fabric'; + this.updateViewModeButton(); + } + + updateViewModeButton() { + if (!this.viewModeButton) return; + const isFabric = this.viewMode === 'fabric'; + this.viewModeButton.textContent = isFabric ? 'VIEW: FABRIC' : 'VIEW: STRIPS'; + this.viewModeButton.style.background = isFabric ? 'rgba(22, 42, 62, 0.94)' : 'rgba(7, 10, 16, 0.92)'; + this.viewModeButton.style.borderColor = isFabric ? 'rgba(138, 198, 255, 0.92)' : 'rgba(178,190,212,0.45)'; + this.viewModeButton.style.color = isFabric ? '#e2f2ff' : '#d5dce8'; + } + onMouseMove(event) { if (!this.canvas) return; const rect = this.canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; + this.lastMouseX = x; + this.lastMouseY = y; this.memoryHoverStrip = null; this.memoryHoverCell = null; + if (this.memoryFabricHits.length) { + for (const hit of this.memoryFabricHits) { + if (x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h) { + this.memoryHoverCell = hit; + this.canvas.style.cursor = 'pointer'; + return; + } + } + } if (this.memoryStripHits.length) { for (const hit of this.memoryStripHits) { if (x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h) { @@ -75,6 +121,22 @@ class MemorySubsystemVisualization { this.canvas.style.cursor = 'default'; } + onCanvasClick(event) { + if (!this.canvas) return; + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + if (this.memoryFabricHits.length) { + for (const hit of this.memoryFabricHits) { + if (x >= hit.x && x <= hit.x + hit.w && y >= hit.y && y <= hit.y + hit.h) { + const same = this.memorySelectedCell && Number(this.memorySelectedCell.pid || 0) === Number(hit.pid || 0); + this.memorySelectedCell = same ? null : hit; + return; + } + } + } + } + fetchTelemetry() { return fetch('/api/processes-realtime', { cache: 'no-store' }) .then((res) => res.json()) @@ -93,7 +155,21 @@ class MemorySubsystemVisualization { available_mb: 0, swap_percent: 0, source: 'fallback' - } + }, + process_pressure: [], + kernel_memory_workers: [], + kernel_memory_state: { + psi_memory: { + some_avg10: 0, + some_avg60: 0, + some_avg300: 0, + full_avg10: 0, + full_avg60: 0, + full_avg300: 0 + }, + vmstat: {}, + psi_factor: 1 + }, } }; }); @@ -137,6 +213,10 @@ class MemorySubsystemVisualization { drawMemoryStats(x, y, w, h) { const sum = this.telemetry?.memory_visual?.summary || {}; + const procPressure = Array.isArray(this.telemetry?.memory_visual?.process_pressure) + ? this.telemetry.memory_visual.process_pressure + : []; + const psiMem = this.telemetry?.memory_visual?.kernel_memory_state?.psi_memory || {}; this.drawPanel(x, y, w, h, 'physical memory ยท /proc/meminfo + psutil', { alpha: 0.88 }); this.ctx.fillStyle = '#c4f8ff'; this.ctx.font = '11px "Share Tech Mono", monospace'; @@ -165,9 +245,24 @@ class MemorySubsystemVisualization { if (line3) { this.ctx.fillText(line3.slice(0, 118), x + 16, y + 96); } + const topProc = procPressure[0]; + if (topProc) { + this.ctx.fillStyle = 'rgba(153, 215, 255, 0.84)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText( + `top pressure ${String(topProc.name || 'proc').slice(0, 16)}:${Number(topProc.pid || 0)} score ${Number(topProc.pressure_score || 0).toFixed(1)} rss ${Number(topProc.rss_mb || 0).toFixed(1)}MB`, + x + 16, + y + 112 + ); + this.ctx.fillText( + `psi mem some10 ${Number(psiMem.some_avg10 || 0).toFixed(2)} full10 ${Number(psiMem.full_avg10 || 0).toFixed(2)}`, + x + Math.max(320, w - 320), + y + 112 + ); + } this.ctx.fillStyle = 'rgba(0, 229, 255, 0.55)'; this.ctx.font = '9px "Share Tech Mono", monospace'; - this.ctx.fillText('strips โ‰ˆ meminfo buckets; task row = RSS share of sampled PIDs; not a physical PFN map', x + 16, y + 112); + this.ctx.fillText('strips โ‰ˆ meminfo buckets; fabric cells bind to sampled process pressure; not a physical PFN map', x + 16, y + 124); } tronHeatColor(t) { @@ -232,6 +327,299 @@ class MemorySubsystemVisualization { } } + drawMemoryFabricView(x, y, w, h) { + const mv = this.telemetry?.memory_visual; + const stripRows = Array.isArray(mv?.rows) ? mv.rows : []; + const summary = mv?.summary || {}; + const processPressure = Array.isArray(mv?.process_pressure) ? mv.process_pressure : []; + const kernelState = mv?.kernel_memory_state || {}; + const psiMem = kernelState.psi_memory || {}; + this.memoryFabricHits = []; + this.drawPanel(x, y, w, h, 'memory fabric map ยท panoramic pressure field', { alpha: 0.9 }); + const innerX = x + 10; + const innerY = y + 30; + const innerW = w - 20; + const innerH = h - 40; + + // Panoramic viewport to match cinematic reference rhythm. + const targetAspect = 2.35; + let viewW = innerW - 16; + let viewH = Math.floor(viewW / targetAspect); + if (viewH > innerH - 14) { + viewH = innerH - 14; + viewW = Math.floor(viewH * targetAspect); + } + const viewX = innerX + Math.floor((innerW - viewW) * 0.5); + const viewY = innerY + Math.floor((innerH - viewH) * 0.5); + + this.ctx.fillStyle = 'rgba(3, 8, 14, 0.88)'; + this.ctx.fillRect(innerX, innerY, innerW, innerH); + this.ctx.fillStyle = 'rgba(5, 11, 18, 0.94)'; + this.ctx.fillRect(viewX, viewY, viewW, viewH); + this.ctx.strokeStyle = 'rgba(118, 166, 220, 0.28)'; + this.ctx.strokeRect(viewX + 0.5, viewY + 0.5, viewW - 1, viewH - 1); + + const cols = Math.max(110, Math.min(220, Math.floor(viewW / 6))); + const rows = Math.max(24, Math.min(74, Math.floor(viewH / 6))); + const cellW = viewW / cols; + const cellH = viewH / rows; + const writeLevel = Math.max(0, Math.min(1, Number(summary.dirty_writeback_mb || 0) / 512)); + const swapLevel = Math.max(0, Math.min(1, Number(summary.swap_percent || 0) / 100)); + const hugeLevel = Math.max(0, Math.min(1, Number(summary.anon_huge_mb || 0) / Math.max(1, Number(summary.total_mb || 1)) * 14)); + + // Flatten rows into weighted fabric source. + const fabricCells = []; + stripRows.forEach((row) => { + const blocks = Array.isArray(row.blocks) ? row.blocks : []; + blocks.forEach((blk) => fabricCells.push({ + kind: blk.kind || row.id || 'anon', + heat: Number(blk.heat || 0), + width: Number(blk.w || 0) + })); + }); + if (!fabricCells.length) { + this.ctx.fillStyle = 'rgba(192, 220, 255, 0.7)'; + this.ctx.font = '11px "Share Tech Mono", monospace'; + this.ctx.fillText('no memory fabric data', viewX + 12, viewY + 24); + return; + } + + const weightedProcPool = []; + processPressure.slice(0, 12).forEach((proc) => { + const score = Math.max(1, Number(proc?.pressure_score || 0)); + const reps = Math.max(1, Math.min(7, Math.floor(score / 18) + 1)); + for (let i = 0; i < reps; i += 1) { + weightedProcPool.push(proc); + } + }); + + const centerX = viewX + viewW * 0.5; + const centerY = viewY + viewH * 0.5; + const maxD = Math.max(1, Math.hypot(viewW * 0.5, viewH * 0.5)); + const hotBias = 0.008 + writeLevel * 0.008 + swapLevel * 0.004; + const hotspotPoints = []; + + for (let ry = 0; ry < rows; ry++) { + for (let cx = 0; cx < cols; cx++) { + const idx = (ry * cols + cx) % fabricCells.length; + const src = fabricCells[idx]; + const px = viewX + cx * cellW; + const py = viewY + ry * cellH; + const noise = (Math.sin(this.tick * 0.028 + cx * 0.33 + ry * 0.21) + 1) * 0.5; + const corridor = 0.5 + 0.5 * Math.sin((cx / cols) * Math.PI * 2 + this.tick * 0.012 + Math.sin((ry / rows) * Math.PI * 2) * 1.2); + const heat = Math.max(0, Math.min(1, src.heat * 0.65 + noise * 0.2 + corridor * 0.15)); + const dx = px - centerX; + const dy = py - centerY; + const edge = Math.max(0, Math.min(1, Math.hypot(dx, dy) / maxD)); + const edgeFalloff = 1 - Math.pow(edge, 1.55); + let alpha = (0.05 + heat * 0.52) * edgeFalloff; + if (src.kind === 'dirty_wb') alpha += writeLevel * 0.25; + if (src.kind === 'swap') alpha += swapLevel * 0.2; + if (src.kind === 'anon_huge' || src.kind === 'shmem_huge') alpha += hugeLevel * 0.16; + alpha = Math.min(0.95, alpha); + const seed = ((cx + 11) * 73856093) ^ ((ry + 17) * 19349663) ^ Math.floor(this.tick * 0.8); + const hotGate = ((seed >>> 0) % 1000) / 1000; + const isHotspot = hotGate > (0.992 - hotBias) && heat > 0.48; + const procOwner = weightedProcPool.length + ? weightedProcPool[(Math.abs((cx * 97 + ry * 53 + (seed >>> 4))) % weightedProcPool.length)] + : null; + + if (src.kind === 'dirty_wb') { + this.ctx.fillStyle = `rgba(${Math.floor(200 + 55 * heat)}, ${Math.floor(140 + 70 * heat)}, ${Math.floor(80 + 28 * heat)}, ${alpha.toFixed(3)})`; + } else if (src.kind === 'cached' || src.kind === 'buffers' || src.kind === 'mapped') { + this.ctx.fillStyle = `rgba(${Math.floor(52 + 32 * heat)}, ${Math.floor(180 + 64 * heat)}, 255, ${alpha.toFixed(3)})`; + } else if (src.kind === 'swap') { + this.ctx.fillStyle = `rgba(${Math.floor(224 + 22 * heat)}, ${Math.floor(105 + 35 * heat)}, ${Math.floor(140 + 35 * heat)}, ${alpha.toFixed(3)})`; + } else { + this.ctx.fillStyle = `rgba(${Math.floor(80 + 80 * heat)}, ${Math.floor(155 + 80 * heat)}, ${Math.floor(200 + 45 * heat)}, ${alpha.toFixed(3)})`; + } + + const rw = Math.max(1, cellW - 1.2); + const rh = Math.max(1, cellH - 1.2); + this.ctx.fillRect(px, py, rw, rh); + if (isHotspot) { + this.ctx.fillStyle = `rgba(255, 250, 232, ${(0.62 + heat * 0.32).toFixed(3)})`; + this.ctx.fillRect(px, py, rw, rh); + if (hotspotPoints.length < 180) { + hotspotPoints.push({ + x: px + rw * 0.5, + y: py + rh * 0.5, + heat + }); + } + } + if (procOwner && (isHotspot || heat > 0.7) && this.memoryFabricHits.length < 700) { + this.memoryFabricHits.push({ + x: px, + y: py, + w: rw, + h: rh, + heat, + kind: src.kind, + pid: Number(procOwner.pid || 0), + name: String(procOwner.name || 'proc'), + role: String(procOwner.role || 'userspace'), + pressure_score: Number(procOwner.pressure_score || 0), + rss_mb: Number(procOwner.rss_mb || 0), + swap_mb: Number(procOwner.swap_mb || 0), + anon_mb: Number(procOwner.anon_mb || 0), + file_mb: Number(procOwner.file_mb || 0), + majflt: Number(procOwner.majflt || 0), + }); + } + } + } + + // Soft bloom around sparse hotspot peaks. + if (hotspotPoints.length) { + this.ctx.save(); + this.ctx.globalCompositeOperation = 'lighter'; + hotspotPoints.forEach((p, i) => { + if (i % 2 !== 0) return; + const rr = 2.6 + p.heat * 6.4; + const glow = this.ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, rr); + glow.addColorStop(0, `rgba(244, 252, 255, ${(0.2 + p.heat * 0.33).toFixed(3)})`); + glow.addColorStop(1, 'rgba(180, 232, 255, 0)'); + this.ctx.fillStyle = glow; + this.ctx.beginPath(); + this.ctx.arc(p.x, p.y, rr, 0, Math.PI * 2); + this.ctx.fill(); + }); + this.ctx.restore(); + } + + // Glow scanline to mimic cinematic analyzer rhythm. + const scanY = viewY + ((Math.sin(this.tick * 0.02) + 1) * 0.5) * viewH; + const grad = this.ctx.createLinearGradient(viewX, scanY - 14, viewX, scanY + 14); + grad.addColorStop(0, 'rgba(120, 210, 255, 0)'); + grad.addColorStop(0.5, 'rgba(146, 232, 255, 0.18)'); + grad.addColorStop(1, 'rgba(120, 210, 255, 0)'); + this.ctx.fillStyle = grad; + this.ctx.fillRect(viewX, scanY - 14, viewW, 28); + + // Vignette and glass pass. + const vignette = this.ctx.createRadialGradient(centerX, centerY, Math.min(viewW, viewH) * 0.2, centerX, centerY, Math.max(viewW, viewH) * 0.72); + vignette.addColorStop(0, 'rgba(0, 0, 0, 0)'); + vignette.addColorStop(1, 'rgba(0, 0, 0, 0.32)'); + this.ctx.fillStyle = vignette; + this.ctx.fillRect(viewX, viewY, viewW, viewH); + const glass = this.ctx.createLinearGradient(viewX, viewY, viewX, viewY + viewH); + glass.addColorStop(0, 'rgba(176, 220, 255, 0.06)'); + glass.addColorStop(0.2, 'rgba(176, 220, 255, 0.01)'); + glass.addColorStop(1, 'rgba(176, 220, 255, 0)'); + this.ctx.fillStyle = glass; + this.ctx.fillRect(viewX, viewY, viewW, viewH); + + // HUD corner markers. + this.ctx.strokeStyle = 'rgba(148, 208, 255, 0.72)'; + this.ctx.lineWidth = 1; + const cm = 16; + this.ctx.beginPath(); + this.ctx.moveTo(viewX + 1, viewY + cm); this.ctx.lineTo(viewX + 1, viewY + 1); this.ctx.lineTo(viewX + cm, viewY + 1); + this.ctx.moveTo(viewX + viewW - cm, viewY + 1); this.ctx.lineTo(viewX + viewW - 1, viewY + 1); this.ctx.lineTo(viewX + viewW - 1, viewY + cm); + this.ctx.moveTo(viewX + 1, viewY + viewH - cm); this.ctx.lineTo(viewX + 1, viewY + viewH - 1); this.ctx.lineTo(viewX + cm, viewY + viewH - 1); + this.ctx.moveTo(viewX + viewW - cm, viewY + viewH - 1); this.ctx.lineTo(viewX + viewW - 1, viewY + viewH - 1); this.ctx.lineTo(viewX + viewW - 1, viewY + viewH - cm); + this.ctx.stroke(); + + // Subtle terminal refresh jitter (rare, low amplitude). + const pulse = ((Math.sin(this.tick * 0.017) + 1) * 0.5); + if (pulse > 0.9) { + const bandCount = 2 + (Math.floor(this.tick) % 2); + for (let i = 0; i < bandCount; i++) { + const by = viewY + Math.floor((((i + 1) * 0.23) + (pulse * 0.19)) * viewH) % Math.max(1, viewH - 10); + const bh = 2 + ((i + Math.floor(this.tick)) % 3); + const shift = ((i % 2 === 0) ? 1 : -1) * (0.8 + pulse * 1.1); + this.ctx.drawImage( + this.canvas, + viewX, + by, + viewW, + bh, + viewX + shift, + by, + viewW, + bh + ); + this.ctx.fillStyle = `rgba(170, 225, 255, ${(0.03 + pulse * 0.05).toFixed(3)})`; + this.ctx.fillRect(viewX, by, viewW, bh); + } + } + + // Thin scanline texture for cinematic monitor feel. + this.ctx.strokeStyle = 'rgba(150, 205, 242, 0.05)'; + this.ctx.lineWidth = 1; + for (let ly = viewY + 1; ly < viewY + viewH; ly += 3) { + this.ctx.beginPath(); + this.ctx.moveTo(viewX, ly + 0.5); + this.ctx.lineTo(viewX + viewW, ly + 0.5); + this.ctx.stroke(); + } + + // Right-side compact activity ruler (HUD micro-bars). + const hudX = viewX + viewW - 10; + const hudY = viewY + 12; + const hudH = Math.max(30, viewH - 24); + const segments = 22; + for (let i = 0; i < segments; i++) { + const t = i / Math.max(1, segments - 1); + const segY = hudY + t * hudH; + const pulse = 0.5 + 0.5 * Math.sin(this.tick * 0.05 + i * 0.7); + const level = Math.max(writeLevel * 0.8, swapLevel * 0.5, hugeLevel * 0.4) * 0.6 + pulse * 0.4; + this.ctx.fillStyle = `rgba(152, 220, 255, ${(0.08 + level * 0.24).toFixed(3)})`; + this.ctx.fillRect(hudX, segY, 4, 2); + } + + this.ctx.fillStyle = 'rgba(160, 204, 232, 0.84)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(`nodes ${rows * cols} ยท hot ${(hotBias * 100).toFixed(1)}% ยท writer ${Math.round(writeLevel * 100)}%`, viewX + 8, viewY - 6); + this.ctx.fillText( + `psi some10 ${Number(psiMem.some_avg10 || 0).toFixed(2)} ยท full10 ${Number(psiMem.full_avg10 || 0).toFixed(2)}`, + viewX + Math.max(180, viewW - 250), + viewY - 6 + ); + this.ctx.fillText('cells represent memory buckets and pressure hotspots, not physical PFN map', viewX + 8, viewY + viewH - 8); + + const tip = this.memorySelectedCell || this.memoryHoverCell; + if (tip && tip.pid) { + this.drawMemoryProcessTooltip(tip, viewX, viewY, viewW, viewH); + } + } + + drawMemoryProcessTooltip(cell, viewX, viewY, viewW, viewH) { + const tw = 274; + const th = 82; + let tx = Math.floor(this.lastMouseX + 16); + let ty = Math.floor(this.lastMouseY + 16); + if (tx + tw > viewX + viewW - 6) tx = viewX + viewW - tw - 8; + if (ty + th > viewY + viewH - 6) ty = viewY + viewH - th - 8; + if (tx < viewX + 6) tx = viewX + 6; + if (ty < viewY + 6) ty = viewY + 6; + + this.drawPanel(tx, ty, tw, th, '', { alpha: 0.93, showTitle: false }); + this.ctx.fillStyle = 'rgba(225, 242, 255, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`${String(cell.name || 'proc').slice(0, 22)} ยท pid ${Number(cell.pid || 0)}`, tx + 10, ty + 16); + this.ctx.fillStyle = 'rgba(166, 203, 236, 0.88)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText( + `${String(cell.role || 'userspace')} ยท score ${Number(cell.pressure_score || 0).toFixed(1)} ยท ${String(cell.kind || 'anon')}`, + tx + 10, + ty + 30 + ); + this.ctx.fillText( + `rss ${Number(cell.rss_mb || 0).toFixed(1)} MB ยท anon ${Number(cell.anon_mb || 0).toFixed(1)} ยท file ${Number(cell.file_mb || 0).toFixed(1)}`, + tx + 10, + ty + 44 + ); + this.ctx.fillText( + `swap ${Number(cell.swap_mb || 0).toFixed(1)} MB ยท majflt ${Math.round(Number(cell.majflt || 0))}`, + tx + 10, + ty + 58 + ); + this.ctx.fillStyle = this.memorySelectedCell ? 'rgba(157, 214, 255, 0.9)' : 'rgba(142, 182, 220, 0.78)'; + this.ctx.fillText(this.memorySelectedCell ? 'selected (click same process to clear)' : 'hover for process pressure details', tx + 10, ty + 72); + } + drawMemoryView(x, y, w, h) { this.memoryMapHit = null; this.memoryStripHits = []; @@ -405,7 +793,15 @@ class MemorySubsystemVisualization { this.ctx.fillRect(0, 0, w, h); this.drawKernelHeader(); this.drawMemoryStats(gap, top, w - gap * 2, statsH); - this.drawMemoryView(gap, graphY, w - gap * 2, graphH); + if (this.viewMode === 'fabric') { + const fabricH = Math.max(220, Math.min(420, Math.floor(graphH * 0.58))); + const stripY = graphY + fabricH + gap; + const stripH = Math.max(160, graphH - fabricH - gap); + this.drawMemoryFabricView(gap, graphY, w - gap * 2, fabricH); + this.drawMemoryView(gap, stripY, w - gap * 2, stripH); + } else { + this.drawMemoryView(gap, graphY, w - gap * 2, graphH); + } } animate() { diff --git a/static/js/proc-3d-visualization.js b/static/js/proc-3d-visualization.js deleted file mode 100644 index 30d53e7..0000000 --- a/static/js/proc-3d-visualization.js +++ /dev/null @@ -1,297 +0,0 @@ -// 3D Visualization of /proc filesystem as a graph inside a coordinate cube -// Integrated into the overall visualization style -class Proc3DVisualization { - constructor() { - this.scene = null; - this.camera = null; - this.renderer = null; - this.cube = null; - this.nodes = []; - this.edges = []; - this.updateInterval = null; - this.container = null; - this.isVisible = false; - this.cameraAngle = 0; - this.cameraSpeed = 0.001; // Very slow drift - } - - init(containerId) { - this.container = document.getElementById(containerId); - if (!this.container) { - console.error('Container not found:', containerId); - return; - } - - // Compact size - bottom right corner - const width = 400; - const height = 400; - - // Create scene - match page background - this.scene = new THREE.Scene(); - this.scene.background = new THREE.Color(0xf2f2f2); // Match body background - - // No fog - keep it clean and minimal - - // Isometric camera - less "3D", more schematic - this.camera = new THREE.OrthographicCamera( - -150, 150, // left, right - 150, -150, // top, bottom - 0.1, 1000 // near, far - ); - - // Isometric angle (classic isometric: 30ยฐ rotation) - const angle = Math.PI / 6; // 30 degrees - this.camera.position.set(200, 200, 200); - this.camera.lookAt(0, 0, 0); - - // Create renderer - match page style - this.renderer = new THREE.WebGLRenderer({ - antialias: true, - alpha: false - }); - this.renderer.setSize(width, height); - this.renderer.setPixelRatio(window.devicePixelRatio); - this.renderer.setClearColor(0xf2f2f2, 1); // Match body background - this.container.appendChild(this.renderer.domElement); - - // Style the canvas to match overall design - this.renderer.domElement.style.border = '1px solid rgba(170, 170, 170, 0.3)'; - this.renderer.domElement.style.borderRadius = '4px'; - this.renderer.domElement.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'; - - // Create minimal cube wireframe - this.createCube(); - - // Subtle lighting - match overall style - const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); - this.scene.add(ambientLight); - - const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3); - directionalLight.position.set(50, 50, 50); - this.scene.add(directionalLight); - - // Start animation loop - this.animate(); - - // Load initial data - this.updateData(); - } - - createCube() { - // Minimal cube wireframe - match overall line style - const geometry = new THREE.BoxGeometry(200, 200, 200); - const edges = new THREE.EdgesGeometry(geometry); - // Match connection-line style: rgba(60, 60, 60, 0.3), stroke-width: 0.8 - const material = new THREE.LineBasicMaterial({ - color: 0x3c3c3c, // #3c3c3c = rgb(60, 60, 60) - linewidth: 1, - opacity: 0.3, - transparent: true - }); - this.cube = new THREE.LineSegments(edges, material); - this.scene.add(this.cube); - } - - updateCameraPosition() { - // Very slow rotation - subtle movement - this.cameraAngle += this.cameraSpeed; - const radius = 250; - const x = Math.cos(this.cameraAngle) * radius; - const z = Math.sin(this.cameraAngle) * radius; - const y = 200 + Math.sin(this.cameraAngle * 0.5) * 30; - - this.camera.position.set(x, y, z); - this.camera.lookAt(0, 0, 0); - } - - updateData() { - fetch('/api/proc-graph') - .then(res => res.json()) - .then(data => { - if (data.error) { - console.error('Error fetching proc graph:', data.error); - return; - } - - debugLog('๐Ÿ“Š Proc graph data:', data.nodes.length, 'nodes', data.edges.length, 'edges'); - - this.renderGraph(data.nodes, data.edges); - }) - .catch(error => { - console.error('Error fetching proc graph:', error); - }); - } - - renderGraph(nodes, edges) { - // Clear existing nodes and edges - const nodesToRemove = this.nodes.map(n => n.mesh); - const edgesToRemove = this.edges.map(e => e.line); - - nodesToRemove.forEach(mesh => this.scene.remove(mesh)); - edgesToRemove.forEach(line => this.scene.remove(line)); - - // Dispose geometries and materials - nodesToRemove.forEach(mesh => { - if (mesh.geometry) mesh.geometry.dispose(); - if (mesh.material) mesh.material.dispose(); - }); - edgesToRemove.forEach(line => { - if (line.geometry) line.geometry.dispose(); - if (line.material) line.material.dispose(); - }); - - this.nodes = []; - this.edges = []; - - // Color palette matching overall style: - // - Dark grays (#333, #444, #555) for most elements - // - Subtle accents for kernel - // - Warm tones for processes (matching process nodes) - - const geometries = { - kernel: new THREE.SphereGeometry(8, 12, 12), - process: new THREE.SphereGeometry(3, 10, 10), - cpu: new THREE.SphereGeometry(3.5, 10, 10), - memory: new THREE.SphereGeometry(3.5, 10, 10), - fd: new THREE.SphereGeometry(2, 8, 8), - irq: new THREE.SphereGeometry(2.5, 8, 8), - network: new THREE.SphereGeometry(3.5, 10, 10), - cgroup: new THREE.SphereGeometry(2.5, 8, 8), - namespace: new THREE.SphereGeometry(2.5, 8, 8), - default: new THREE.SphereGeometry(2, 8, 8) - }; - - const materials = { - // Kernel - subtle dark accent (like central circle) - kernel: new THREE.MeshPhongMaterial({ - color: 0x333333, // Match .node-circle fill: #555, stroke: #222 - emissive: 0x111111, - emissiveIntensity: 0.2 - }), - // Processes - warm gray (matching process nodes) - process: new THREE.MeshPhongMaterial({ - color: 0x555555, // Match .node-circle fill - emissive: 0x333333, - emissiveIntensity: 0.1 - }), - // Resources - cool grays - cpu: new THREE.MeshPhongMaterial({ - color: 0x444444, // Match .socket-text fill - emissive: 0x222222, - emissiveIntensity: 0.1 - }), - memory: new THREE.MeshPhongMaterial({ - color: 0x444444, - emissive: 0x222222, - emissiveIntensity: 0.1 - }), - fd: new THREE.MeshPhongMaterial({ - color: 0x444444, - emissive: 0x222222, - emissiveIntensity: 0.1 - }), - irq: new THREE.MeshPhongMaterial({ - color: 0x444444, - emissive: 0x222222, - emissiveIntensity: 0.1 - }), - network: new THREE.MeshPhongMaterial({ - color: 0x444444, - emissive: 0x222222, - emissiveIntensity: 0.1 - }), - cgroup: new THREE.MeshPhongMaterial({ - color: 0x444444, - emissive: 0x222222, - emissiveIntensity: 0.1 - }), - namespace: new THREE.MeshPhongMaterial({ - color: 0x444444, - emissive: 0x222222, - emissiveIntensity: 0.1 - }), - default: new THREE.MeshPhongMaterial({ - color: 0x444444, - emissive: 0x222222, - emissiveIntensity: 0.1 - }) - }; - - nodes.forEach(nodeData => { - const type = nodeData.type || 'default'; - const geometry = geometries[type] || geometries.default; - const material = materials[type] || materials.default; - - const mesh = new THREE.Mesh(geometry, material); - mesh.position.set(nodeData.x, nodeData.y, nodeData.z); - mesh.userData = { label: nodeData.label || nodeData.id }; - - this.scene.add(mesh); - this.nodes.push({ mesh, data: nodeData }); - }); - - // Create edge lines - match curve-path style - edges.forEach(edgeData => { - const fromNode = nodes.find(n => n.id === edgeData.from); - const toNode = nodes.find(n => n.id === edgeData.to); - - if (fromNode && toNode) { - const geometry = new THREE.BufferGeometry().setFromPoints([ - new THREE.Vector3(fromNode.x, fromNode.y, fromNode.z), - new THREE.Vector3(toNode.x, toNode.y, toNode.z) - ]); - - // Match .curve-path style: stroke: rgba(100, 100, 100, 0.2), stroke-width: 0.7 - const color = 0x646464; // #646464 = rgb(100, 100, 100) - const opacity = 0.2; - - const material = new THREE.LineBasicMaterial({ - color: color, - opacity: opacity, - transparent: true - }); - const line = new THREE.Line(geometry, material); - - this.scene.add(line); - this.edges.push({ line, data: edgeData }); - } - }); - } - - animate() { - requestAnimationFrame(() => this.animate()); - - if (this.isVisible && this.renderer) { - // Very slow camera drift - this.updateCameraPosition(); - - this.renderer.render(this.scene, this.camera); - } - } - - show() { - if (this.container) { - this.container.style.display = 'block'; - this.isVisible = true; - this.updateData(); // Refresh data when shown - } - } - - hide() { - if (this.container) { - this.container.style.display = 'none'; - this.isVisible = false; - } - } - - destroy() { - if (this.updateInterval) { - clearInterval(this.updateInterval); - } - if (this.renderer) { - this.renderer.dispose(); - } - this.nodes = []; - this.edges = []; - } -} diff --git a/static/js/processes-belt.js b/static/js/processes-belt.js index 36bb2ae..60625c1 100644 --- a/static/js/processes-belt.js +++ b/static/js/processes-belt.js @@ -21,9 +21,17 @@ class ProcessesSubsystemVisualization { this.filterButtons = new Map(); this.overlayNodes = []; this.hoveredNodePid = null; + this.selectedNodePid = null; + this.lastMicroscopeFocusPid = null; + this.autoFocusEnabled = true; + this.autoFocusButton = null; this.nodeHitAreas = []; this.positionHistory = []; + this.processHistory = new Map(); + this.processHistoryWindowMs = 60000; + this.processHistoryMaxPoints = 90; this.mouseMoveHandler = null; + this.clickHandler = null; } init(containerId = 'processes-belt-container') { @@ -45,7 +53,9 @@ class ProcessesSubsystemVisualization { this.container.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); this.mouseMoveHandler = (event) => this.onMouseMove(event); + this.clickHandler = (event) => this.onCanvasClick(event); this.canvas.addEventListener('mousemove', this.mouseMoveHandler); + this.canvas.addEventListener('click', this.clickHandler); this.onResize(); window.addEventListener('resize', () => this.onResize()); @@ -62,6 +72,7 @@ class ProcessesSubsystemVisualization { this.overlayNodes.push(this.exitButton); this.createModeToggle(); + this.createAutoFocusToggle(); this.createEdgeFilterToggle(); return true; } @@ -73,7 +84,8 @@ class ProcessesSubsystemVisualization { `; const modes = [ { key: 'temporal', label: '3-LAYER GRAPH' }, - { key: 'radial', label: 'RADIAL GRAPH' } + { key: 'radial', label: 'RADIAL GRAPH' }, + { key: 'microscope', label: 'MICROSCOPE' } ]; modes.forEach((m) => { const btn = document.createElement('button'); @@ -94,13 +106,63 @@ class ProcessesSubsystemVisualization { } setLayoutMode(modeKey) { - this.layoutMode = (modeKey === 'radial') ? 'radial' : 'temporal'; + this.layoutMode = ['temporal', 'radial', 'microscope'].includes(modeKey) ? modeKey : 'temporal'; this.modeButtons.forEach((btn, key) => { const active = key === this.layoutMode; btn.style.background = active ? 'rgba(32, 52, 81, 0.92)' : 'rgba(8,12,18,0.86)'; btn.style.borderColor = active ? 'rgba(124, 178, 255, 0.9)' : 'rgba(150,164,188,0.35)'; btn.style.color = active ? '#d9ecff' : '#bcc8db'; }); + this.updateAutoFocusButtonState(); + } + + createAutoFocusToggle() { + const btn = document.createElement('button'); + btn.style.cssText = ` + position:absolute;top:18px;left:392px;z-index:1001; + padding:8px 10px;background:rgba(8,12,18,0.86); + border:1px solid rgba(150,164,188,0.35);color:#bcc8db; + font-family:'Share Tech Mono', monospace;font-size:10px;cursor:pointer; + `; + btn.onclick = () => this.toggleAutoFocus(); + this.container.appendChild(btn); + this.autoFocusButton = btn; + this.overlayNodes.push(btn); + this.updateAutoFocusButtonState(); + } + + toggleAutoFocus() { + this.autoFocusEnabled = !this.autoFocusEnabled; + this.updateAutoFocusButtonState(); + } + + updateAutoFocusButtonState() { + if (!this.autoFocusButton) return; + const active = this.autoFocusEnabled; + const visible = this.layoutMode === 'microscope'; + this.autoFocusButton.textContent = active ? 'AUTO FOCUS: ON' : 'AUTO FOCUS: OFF'; + this.autoFocusButton.style.background = active ? 'rgba(32, 52, 81, 0.92)' : 'rgba(8,12,18,0.86)'; + this.autoFocusButton.style.borderColor = active ? 'rgba(124,178,255,0.9)' : 'rgba(150,164,188,0.35)'; + this.autoFocusButton.style.color = active ? '#d9ecff' : '#bcc8db'; + this.autoFocusButton.style.opacity = visible ? '1' : '0.35'; + this.autoFocusButton.style.pointerEvents = visible ? 'auto' : 'none'; + } + + onCanvasClick(event) { + if (!this.canvas) return; + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + for (const hit of this.nodeHitAreas) { + const dx = x - hit.x; + const dy = y - hit.y; + if (Math.hypot(dx, dy) <= hit.r + 3) { + const pid = Number(hit.pid || 0); + if (pid <= 0) return; + this.selectedNodePid = this.selectedNodePid === pid ? null : pid; + return; + } + } } createEdgeFilterToggle() { @@ -162,12 +224,76 @@ class ProcessesSubsystemVisualization { this.canvas.style.cursor = hovered ? 'pointer' : 'default'; } + updateProcessHistory() { + const now = Date.now(); + const cutoff = now - this.processHistoryWindowMs; + const nodes = Array.isArray(this.telemetry?.neural_graph?.nodes) ? this.telemetry.neural_graph.nodes : []; + const keepPids = new Set(); + nodes.forEach((node) => { + const pid = Number(node?.pid || 0); + if (pid <= 0) return; + keepPids.add(pid); + const row = { + ts: now, + syscall_pressure: Number(node?.syscall_pressure || 0), + rss_mb: Number(node?.rss_bytes || 0) / (1024 * 1024), + fd_count: Number(node?.fd_count || 0), + connections: Number(node?.connections || 0), + }; + const arr = this.processHistory.get(pid) || []; + arr.push(row); + this.processHistory.set( + pid, + arr.filter((entry) => Number(entry.ts || 0) >= cutoff).slice(-this.processHistoryMaxPoints) + ); + }); + for (const pid of this.processHistory.keys()) { + if (!keepPids.has(pid) && pid !== this.selectedNodePid) { + this.processHistory.delete(pid); + } + } + } + + getFocusProcess(nodes) { + if (!Array.isArray(nodes) || !nodes.length) return null; + if (this.selectedNodePid) { + const selected = nodes.find((n) => Number(n?.pid || 0) === Number(this.selectedNodePid)); + if (selected) { + this.lastMicroscopeFocusPid = Number(selected.pid || 0); + return selected; + } + this.selectedNodePid = null; + } + if (this.autoFocusEnabled) { + const top = nodes.slice().sort((a, b) => Number(b?.syscall_pressure || 0) - Number(a?.syscall_pressure || 0))[0]; + if (top) { + this.lastMicroscopeFocusPid = Number(top.pid || 0); + } + return top || nodes[0] || null; + } + if (this.lastMicroscopeFocusPid) { + const prev = nodes.find((n) => Number(n?.pid || 0) === Number(this.lastMicroscopeFocusPid)); + if (prev) return prev; + } + if (this.hoveredNodePid) { + const hovered = nodes.find((n) => Number(n?.pid || 0) === Number(this.hoveredNodePid)); + if (hovered) { + this.lastMicroscopeFocusPid = Number(hovered.pid || 0); + return hovered; + } + } + const fallback = nodes[0] || null; + if (fallback) this.lastMicroscopeFocusPid = Number(fallback.pid || 0); + return fallback; + } + fetchTelemetry() { return fetch('/api/processes-realtime', { cache: 'no-store' }) .then((res) => res.json()) .then((data) => { if (!data || data.error) throw new Error(data?.error || 'No data'); this.telemetry = data; + this.updateProcessHistory(); }) .catch(() => { this.telemetry = { @@ -215,7 +341,7 @@ class ProcessesSubsystemVisualization { this.ctx.beginPath(); this.ctx.moveTo(vpX - areaW * spread, yy); this.ctx.lineTo(vpX + areaW * spread, yy); - this.ctx.strokeStyle = `rgba(130, 185, 255, ${0.06 + t * 0.28})`; + this.ctx.strokeStyle = `rgba(142, 224, 255, ${0.04 + t * 0.2})`; this.ctx.lineWidth = 1; this.ctx.stroke(); } @@ -225,7 +351,7 @@ class ProcessesSubsystemVisualization { this.ctx.beginPath(); this.ctx.moveTo(vpX, vpY); this.ctx.lineTo(xb, areaY + areaH); - this.ctx.strokeStyle = 'rgba(110, 165, 235, 0.14)'; + this.ctx.strokeStyle = 'rgba(122, 205, 242, 0.11)'; this.ctx.stroke(); } } @@ -287,6 +413,9 @@ class ProcessesSubsystemVisualization { this.ctx.fillStyle = 'rgba(232, 240, 252, 0.92)'; this.ctx.font = '12px "Share Tech Mono", monospace'; this.ctx.fillText('linux kernel ยท process management subsystem', w * 0.5, 26); + this.ctx.fillStyle = 'rgba(155, 200, 232, 0.78)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText('SCANNING NODE', w * 0.5, 16); this.ctx.textAlign = 'start'; } @@ -366,11 +495,11 @@ class ProcessesSubsystemVisualization { } edgeColor(type) { - if (type === 'syscalls') return 'rgba(138,156,234,0.42)'; - if (type === 'ipc') return 'rgba(96,214,157,0.42)'; - if (type === 'network') return 'rgba(244,201,119,0.46)'; - if (type === 'file_access') return 'rgba(235,126,126,0.44)'; - return 'rgba(165,178,200,0.38)'; + if (type === 'syscalls') return 'rgba(136,201,255,0.4)'; + if (type === 'ipc') return 'rgba(126,242,210,0.38)'; + if (type === 'network') return 'rgba(181,240,255,0.42)'; + if (type === 'file_access') return 'rgba(255,184,168,0.4)'; + return 'rgba(170,214,244,0.34)'; } stableUnit(n) { @@ -379,7 +508,7 @@ class ProcessesSubsystemVisualization { } drawNeuralGraph(x, y, w, h) { - this.drawPanel(x, y, w, 36, 'task graph ยท fork / wait / signals / IO', { alpha: 0.9 }); + this.drawPanel(x, y, w, 36, 'process microscope ยท fork / wait / signals / io', { alpha: 0.9 }); const innerY = y + 42; const innerH = h - 50; const graph = this.telemetry?.neural_graph || {}; @@ -405,6 +534,10 @@ class ProcessesSubsystemVisualization { this.drawRadialNeuralGraph(nodes, edges, pos, x, innerY, w, innerH); return; } + if (this.layoutMode === 'microscope') { + this.drawMicroscopeGraph(nodes, edges, x, innerY, w, innerH); + return; + } this.nodeHitAreas = []; const leftX = x + w * 0.2; @@ -692,6 +825,180 @@ class ProcessesSubsystemVisualization { this.ctx.fillText('radial: task ring ยท curved edges ยท hover neighborhood', x + 14, y + h - 14); } + drawMicroscopeTimeline(focusNode, x, y, w, h) { + const pid = Number(focusNode?.pid || 0); + if (!pid) return; + const points = this.processHistory.get(pid) || []; + this.drawPanel(x, y, w, h, '', { alpha: 0.84, showTitle: false }); + this.ctx.fillStyle = 'rgba(198, 220, 248, 0.92)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(`timeline 60s ยท pid ${pid}`, x + 10, y + 14); + if (points.length < 2) return; + const now = Date.now(); + const plotX = x + 10; + const plotY = y + 20; + const plotW = w - 20; + const plotH = h - 30; + const drawSeries = (getValue, color, alpha = 0.95) => { + this.ctx.beginPath(); + points.forEach((pt, idx) => { + const age = Math.max(0, now - Number(pt.ts || now)); + const px = plotX + (1 - Math.min(1, age / this.processHistoryWindowMs)) * plotW; + const v = Math.max(0, Math.min(100, Number(getValue(pt) || 0))); + const py = plotY + plotH - (v / 100) * plotH; + if (idx === 0) this.ctx.moveTo(px, py); + else this.ctx.lineTo(px, py); + }); + this.ctx.strokeStyle = color; + this.ctx.globalAlpha = alpha; + this.ctx.lineWidth = 1.2; + this.ctx.stroke(); + this.ctx.globalAlpha = 1; + }; + drawSeries((pt) => pt.syscall_pressure, 'rgba(150, 214, 255, 0.95)'); + drawSeries((pt) => Math.min(100, pt.rss_mb / 12), 'rgba(120, 230, 170, 0.85)', 0.86); + drawSeries((pt) => Math.min(100, pt.fd_count * 2), 'rgba(255, 205, 128, 0.82)', 0.8); + } + + drawMicroscopeGraph(nodes, edges, x, y, w, h) { + this.drawPanel(x, y, w, 36, 'process microscope ยท parent/child ยท threads ยท interactions', { alpha: 0.9 }); + const innerY = y + 42; + const innerH = h - 50; + this.nodeHitAreas = []; + if (!nodes.length) { + this.ctx.fillStyle = 'rgba(196,207,224,0.72)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('NO PROCESS GRAPH DATA', x + 20, innerY + 28); + return; + } + const byPid = new Map(nodes.map((n) => [Number(n.pid || 0), n])); + const focus = this.getFocusProcess(nodes); + if (!focus) return; + const focusPid = Number(focus.pid || 0); + const centerX = x + w * 0.5; + const centerY = innerY + innerH * 0.42; + const parent = byPid.get(Number(focus.ppid || 0)) || null; + const children = nodes.filter((n) => Number(n.ppid || 0) === focusPid).slice(0, 6); + const neighbors = []; + edges.forEach((edge) => { + const s = Number(edge.source || 0); + const t = Number(edge.target || 0); + if (s === focusPid && byPid.has(t)) neighbors.push({ node: byPid.get(t), type: String(edge.type || 'link') }); + if (t === focusPid && byPid.has(s)) neighbors.push({ node: byPid.get(s), type: String(edge.type || 'link') }); + }); + const uniqNeighbors = []; + const seen = new Set(); + neighbors.forEach((it) => { + const pid = Number(it?.node?.pid || 0); + if (pid <= 0 || seen.has(pid) || pid === focusPid) return; + seen.add(pid); + uniqNeighbors.push(it); + }); + const threadsCount = Math.max(1, Number(focus.num_threads || 1)); + const ringA = Math.min(w, innerH) * 0.18; + const ringB = Math.min(w, innerH) * 0.29; + const ringC = Math.min(w, innerH) * 0.4; + + // semantic rings + [ringA, ringB, ringC].forEach((r, idx) => { + this.ctx.beginPath(); + this.ctx.arc(centerX, centerY, r, 0, Math.PI * 2); + this.ctx.strokeStyle = idx === 2 ? 'rgba(120,170,230,0.18)' : 'rgba(120,170,230,0.24)'; + this.ctx.lineWidth = idx === 0 ? 1.1 : 0.9; + this.ctx.stroke(); + }); + + const nodePos = new Map(); + const placeNode = (node, px, py, radius, fill, label) => { + this.ctx.beginPath(); + this.ctx.arc(px, py, radius, 0, Math.PI * 2); + this.ctx.fillStyle = fill; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(228, 239, 255, 0.92)'; + this.ctx.lineWidth = 1; + this.ctx.stroke(); + this.ctx.fillStyle = '#eaf1fb'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(label, px + radius + 6, py + 3); + const pid = Number(node?.pid || 0); + if (pid > 0) this.nodeHitAreas.push({ x: px, y: py, r: radius, pid }); + if (pid > 0) nodePos.set(pid, { x: px, y: py }); + }; + + // center focus + placeNode( + focus, + centerX, + centerY, + 11, + 'rgba(163, 220, 255, 0.9)', + `${String(focus.name || 'proc').slice(0, 14)}:${focusPid}` + ); + this.selectedNodePid = focusPid; + + // parent/children ring + if (parent) { + const px = centerX; + const py = centerY - ringA; + placeNode(parent, px, py, 7, 'rgba(192, 210, 238, 0.85)', `parent ${String(parent.name || '').slice(0, 10)}:${Number(parent.pid || 0)}`); + } + children.forEach((ch, idx) => { + const a = (Math.PI * 2 * idx / Math.max(1, children.length)) - Math.PI / 2; + const px = centerX + Math.cos(a) * ringA; + const py = centerY + Math.sin(a) * ringA; + placeNode(ch, px, py, 6.2, 'rgba(152, 204, 255, 0.82)', `child ${String(ch.name || '').slice(0, 8)}:${Number(ch.pid || 0)}`); + }); + + // threads ring (synthetic thread beads) + const threadsToDraw = Math.min(16, Math.max(3, threadsCount)); + for (let i = 0; i < threadsToDraw; i++) { + const a = (Math.PI * 2 * i / threadsToDraw) + this.tick * 0.005; + const px = centerX + Math.cos(a) * ringB; + const py = centerY + Math.sin(a) * ringB; + this.ctx.beginPath(); + this.ctx.arc(px, py, 2.1, 0, Math.PI * 2); + this.ctx.fillStyle = 'rgba(120, 230, 170, 0.85)'; + this.ctx.fill(); + } + + // interactions ring + uniqNeighbors.slice(0, 10).forEach((item, idx) => { + const n = item.node; + const a = (Math.PI * 2 * idx / Math.max(1, Math.min(10, uniqNeighbors.length))) + Math.PI * 0.06; + const px = centerX + Math.cos(a) * ringC; + const py = centerY + Math.sin(a) * ringC; + const edgeType = String(item.type || ''); + const color = edgeType === 'network' ? 'rgba(244,201,119,0.88)' + : (edgeType === 'ipc' ? 'rgba(96,214,157,0.88)' + : (edgeType === 'file_access' ? 'rgba(235,126,126,0.88)' : 'rgba(160,180,210,0.85)')); + placeNode(n, px, py, 5.2, color, `${edgeType || 'link'} ${String(n.name || '').slice(0, 8)}:${Number(n.pid || 0)}`); + }); + + // links from focus to related + for (const [pid, p] of nodePos.entries()) { + if (pid === focusPid) continue; + this.drawCurvedStroke(centerX, centerY, p.x, p.y, 'rgba(204, 224, 248, 0.38)', 0.9, focusPid * 17 + pid, false); + } + + const leftX = x + 16; + const detailY = innerY + innerH - 110; + this.drawPanel(leftX, detailY, Math.min(390, w * 0.42), 96, '', { alpha: 0.84, showTitle: false }); + this.ctx.fillStyle = 'rgba(212, 232, 255, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.fillText(`focus ${String(focus.name || 'proc').slice(0, 18)}:${focusPid}`, leftX + 10, detailY + 18); + this.ctx.fillStyle = 'rgba(166, 195, 226, 0.9)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(`pressure ${Number(focus.syscall_pressure || 0).toFixed(0)} ยท rss ${(Number(focus.rss_bytes || 0) / (1024 * 1024)).toFixed(1)}MB`, leftX + 10, detailY + 36); + this.ctx.fillText(`threads ${Number(focus.num_threads || 0)} ยท fd ${Number(focus.fd_count || 0)} ยท seccomp ${String(focus.seccomp_mode || 'unknown')}`, leftX + 10, detailY + 52); + this.ctx.fillText(`parent ${parent ? `${String(parent.name || '').slice(0, 10)}:${Number(parent.pid || 0)}` : 'none'} ยท children ${children.length}`, leftX + 10, detailY + 68); + this.ctx.fillText(`interactions ${uniqNeighbors.length} (ipc/network/file_access)`, leftX + 10, detailY + 84); + + this.drawMicroscopeTimeline(focus, x + w * 0.56, innerY + innerH - 96, w * 0.4, 84); + this.ctx.fillStyle = 'rgba(140, 158, 188, 0.85)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('click node: pin focus ยท rings: parent/child, threads, interactions', x + 12, y + h - 10); + } + drawTopStats(x, y, w, h) { this.drawPanel(x, y, w, h, 'runtime snapshot'); const meta = this.telemetry?.meta || {}; @@ -707,6 +1014,41 @@ class ProcessesSubsystemVisualization { this.ctx.fillText('vertices: processes ยท edges: syscalls ยท ipc ยท sockets ยท vfs', x + 16, y + 72); } + drawHudChrome(x, y, w, h) { + const cx = x + w * 0.5; + const cy = y + h * 0.5; + this.ctx.strokeStyle = 'rgba(150, 222, 255, 0.2)'; + this.ctx.lineWidth = 1; + this.ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1); + + // Side bracket arcs inspired by tactical HUD layouts. + const arcR = Math.min(w, h) * 0.34; + this.ctx.beginPath(); + this.ctx.arc(cx - w * 0.22, cy, arcR, -0.7, 0.7); + this.ctx.arc(cx + w * 0.22, cy, arcR, Math.PI - 0.7, Math.PI + 0.7); + this.ctx.strokeStyle = 'rgba(140, 220, 255, 0.24)'; + this.ctx.stroke(); + + // Thin horizontal targeting bars. + this.ctx.strokeStyle = 'rgba(150, 226, 255, 0.22)'; + this.ctx.beginPath(); + this.ctx.moveTo(x + 26, cy); + this.ctx.lineTo(cx - 80, cy); + this.ctx.moveTo(cx + 80, cy); + this.ctx.lineTo(x + w - 26, cy); + this.ctx.stroke(); + + // Small center reticle. + this.ctx.beginPath(); + this.ctx.arc(cx, cy, 18, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(178, 236, 255, 0.42)'; + this.ctx.stroke(); + this.ctx.beginPath(); + this.ctx.arc(cx, cy, 7, 0, Math.PI * 2); + this.ctx.strokeStyle = 'rgba(208, 247, 255, 0.9)'; + this.ctx.stroke(); + } + drawScene() { if (!this.ctx || !this.canvas) return; const w = window.innerWidth; @@ -721,12 +1063,30 @@ class ProcessesSubsystemVisualization { const graphH = Math.max(260, h - graphY - 36); const bg = this.ctx.createRadialGradient(w * 0.5, h * 0.38, 0, w * 0.5, h * 0.38, Math.max(w, h) * 0.78); - bg.addColorStop(0, '#0f1a2e'); - bg.addColorStop(0.55, '#070d18'); - bg.addColorStop(1, '#03060e'); + bg.addColorStop(0, '#0e1722'); + bg.addColorStop(0.55, '#060b13'); + bg.addColorStop(1, '#02050b'); this.ctx.fillStyle = bg; this.ctx.fillRect(0, 0, w, h); + // Global subtle matrix grid. + this.ctx.strokeStyle = 'rgba(120, 188, 235, 0.08)'; + this.ctx.lineWidth = 1; + for (let gx = 0; gx < w; gx += 36) { + this.ctx.beginPath(); + this.ctx.moveTo(gx + 0.5, 0); + this.ctx.lineTo(gx + 0.5, h); + this.ctx.stroke(); + } + for (let gy = 0; gy < h; gy += 36) { + this.ctx.beginPath(); + this.ctx.moveTo(0, gy + 0.5); + this.ctx.lineTo(w, gy + 0.5); + this.ctx.stroke(); + } + + this.drawHudChrome(gap, graphY, w - gap * 2, graphH); + this.drawKernelHeader(); this.drawTopStats(gap, top, w - gap * 2, statsH); this.drawNeuralGraph(gap, graphY, w - gap * 2, graphH);