diff --git a/app.py b/app.py index ecc2853..406ae8b 100644 --- a/app.py +++ b/app.py @@ -750,6 +750,16 @@ def processes_page_legacy(): """Legacy path redirect to Linux processes subsystem page.""" return redirect('/linux-processes-subsystem', code=301) +@app.route('/linux-memory-subsystem') +def linux_memory_subsystem_page(): + """SEO-friendly Linux memory subsystem page.""" + return render_template('linux-memory-subsystem.html') + + +@app.route('/linux-memory-subsystem.html') +def linux_memory_subsystem_html(): + return redirect('/linux-memory-subsystem', code=301) + @app.route('/api/syscalls-realtime') def syscalls_realtime(): """API for real-time system calls""" @@ -4458,6 +4468,151 @@ def _read_text(path): } } +def _parse_meminfo_kb(): + """Linux /proc/meminfo values in kB (same units as psutil docs).""" + out = {} + try: + with open("/proc/meminfo", "r", encoding="utf-8", errors="ignore") as f: + for line in f: + parts = line.split() + if len(parts) >= 2 and parts[1].isdigit(): + out[parts[0].rstrip(":")] = int(parts[1]) + except Exception: + pass + return out + + +def _memory_strip_blocks(kind, kb_k, mem_total_kb, n_blocks, seed0): + """Variable-width blocks in one horizontal strip; heat ~ share of RAM in this category.""" + mem_total_kb = max(1, int(mem_total_kb)) + kb_k = max(0, int(kb_k)) + weights = [] + for i in range(n_blocks): + v = (((seed0 + i * 104729) % 1000) + 40) / 1040.0 + weights.append(v) + sw = sum(weights) + share = kb_k / float(mem_total_kb) + blocks = [] + for i in range(n_blocks): + w = weights[i] / sw + heat = min( + 1.0, + 0.05 + min(0.92, share * 2.0) + (((seed0 + i * 31) % 15) / 120.0), + ) + blocks.append({"w": round(w, 6), "heat": round(heat, 4), "kind": kind}) + return blocks + + +def _build_memory_visual_rows(meminfo_kb, syscall_nodes, vm, swap): + """ + Rows of horizontal strips: each row ≈ one kernel/accounting bucket from /proc/meminfo. + Block widths are stylistic subdivisions; row mass is proportional to kb / MemTotal. + """ + mi = meminfo_kb or {} + mt = max(1, int(mi.get("MemTotal") or 0)) + if mt <= 1: + try: + mt = max(1, int(getattr(vm, "total", 0) / 1024)) + except Exception: + mt = 1 + + seed_base = (mt % 100000) + int(mi.get("Active", 0) or 0) % 50000 + + row_specs = [ + ("buffers", "buffers", mi.get("Buffers", 0)), + ("cached", "page cache", mi.get("Cached", 0)), + ("anon", "anonymous (heap/stack)", mi.get("AnonPages", 0)), + ("slab", "slab / kmalloc", mi.get("Slab", 0)), + ("shmem", "shmem / tmpfs", mi.get("Shmem", 0)), + ("mapped", "file mappings", mi.get("Mapped", 0)), + ] + swap_tot = int(mi.get("SwapTotal", 0) or 0) + swap_free = int(mi.get("SwapFree", 0) or 0) + swap_used = max(0, swap_tot - swap_free) + if swap_tot > 0: + row_specs.append(("swap", "swap occupied", swap_used)) + + pt = int(mi.get("PageTables", 0) or 0) + ks = int(mi.get("KernelStack", 0) or 0) + if pt + ks > 0: + row_specs.append(("kmeta", "pagetables + kernel stacks", pt + ks)) + + rows = [] + for sk, label, kb in row_specs: + kb = int(kb or 0) + if kb <= 0 and sk != "swap": + continue + if sk == "swap" and kb <= 0: + continue + sk_seed = sum(ord(c) for c in sk) * 31 + len(sk) + n_blocks = 22 + (seed_base % 11) + (sk_seed % 9) + seed0 = seed_base + (sk_seed % 100000) + blocks = _memory_strip_blocks(sk, kb, mt, n_blocks, seed0) + rows.append( + { + "id": sk, + "label": label, + "kb": kb, + "pct_of_ram": round(100.0 * kb / float(mt), 2) if mt else 0.0, + "blocks": blocks, + } + ) + + top_tasks = sorted( + syscall_nodes, + key=lambda x: int(x.get("rss_bytes") or 0), + reverse=True, + )[:6] + if top_tasks: + tr_bytes = sum(int(x.get("rss_bytes") or 0) for x in top_tasks) or 1 + tr_kb = max(1, int(tr_bytes / 1024)) + task_blocks = [] + for p in top_tasks: + rss = int(p.get("rss_bytes") or 0) + if rss <= 0: + continue + w = rss / float(tr_bytes) + mp = float(p.get("memory_percent") or 0.0) + heat = min(1.0, 0.2 + (mp / 100.0) * 0.75 + (rss / float(tr_bytes)) * 0.15) + task_blocks.append( + { + "w": round(w, 6), + "heat": round(heat, 4), + "kind": "task", + "pid": int(p.get("pid") or 0), + "name": str(p.get("name") or "")[:14], + } + ) + if task_blocks: + sw = sum(b["w"] for b in task_blocks) + if sw > 0: + for b in task_blocks: + b["w"] = round(b["w"] / sw, 6) + rows.append( + { + "id": "tasks", + "label": "sampled tasks RSS (top)", + "kb": tr_kb, + "pct_of_ram": round(100.0 * tr_kb / float(mt), 2) if mt else 0.0, + "blocks": task_blocks, + } + ) + + summary = { + "total_mb": round(mt / 1024.0, 1), + "used_percent": round(vm.percent, 1) if vm else 0.0, + "available_mb": round((mi.get("MemAvailable", 0) or 0) / 1024.0, 1), + "swap_percent": round(swap.percent, 1) if swap else 0.0, + "buffers_mb": round((mi.get("Buffers", 0) or 0) / 1024.0, 1), + "cached_mb": round((mi.get("Cached", 0) or 0) / 1024.0, 1), + "anon_mb": round((mi.get("AnonPages", 0) or 0) / 1024.0, 1), + "slab_mb": round((mi.get("Slab", 0) or 0) / 1024.0, 1), + "swap_used_mb": round(swap_used / 1024.0, 1) if swap_tot else 0.0, + "source": "proc_meminfo+psutil", + } + return rows, summary + + def collect_processes_realtime(): """ Processes subsystem telemetry focused on: @@ -4493,6 +4648,11 @@ def collect_processes_realtime(): cpu = float(proc.info.get("cpu_percent") or 0.0) mem = float(proc.info.get("memory_percent") or 0.0) threads = int(proc.info.get("num_threads") or 0) + rss = 0 + try: + rss = int(getattr(proc.memory_info(), "rss", 0) or 0) + except Exception: + rss = 0 fd_count = 0 try: fd_count = int(proc.num_fds() or 0) @@ -4521,7 +4681,9 @@ def collect_processes_realtime(): "user": user, "fd_count": fd_count, "syscall_pressure": syscall_pressure, - "seccomp_mode": seccomp_mode + "seccomp_mode": seccomp_mode, + "memory_percent": round(mem, 2), + "rss_bytes": rss, }) except Exception: continue @@ -4623,7 +4785,9 @@ def collect_processes_realtime(): "fd_count": int(row.get("fd_count") or 0), "seccomp_mode": str(row.get("seccomp_mode") or "unknown"), "connections": 0, - "unique_peers": 0 + "unique_peers": 0, + "memory_percent": float(row.get("memory_percent") or 0.0), + "rss_bytes": int(row.get("rss_bytes") or 0), } for row in network_tracing[:16]: pid = int(row.get("pid") or 0) @@ -4639,7 +4803,9 @@ def collect_processes_realtime(): "fd_count": 0, "seccomp_mode": "unknown", "connections": 0, - "unique_peers": 0 + "unique_peers": 0, + "memory_percent": 0.0, + "rss_bytes": 0, } node_pool[pid]["connections"] = int(row.get("connections") or 0) node_pool[pid]["unique_peers"] = int(row.get("unique_peers") or 0) @@ -4710,6 +4876,29 @@ def _add_edge(src_pid, dst_pid, edge_type, weight): nodes = list(node_pool.values())[:18] edges = edges[:64] + try: + vm = psutil.virtual_memory() + 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, + } + except Exception: + memory_visual = { + "layout": "strips", + "rows": [], + "summary": { + "total_mb": 0, + "used_percent": 0.0, + "available_mb": 0, + "swap_percent": 0.0, + "source": "error", + }, + } + return { "timestamp": datetime.utcnow().isoformat() + "Z", "syscalls_interception": syscall_nodes, @@ -4719,6 +4908,7 @@ def _add_edge(src_pid, dst_pid, edge_type, weight): "nodes": nodes, "edges": edges }, + "memory_visual": memory_visual, "meta": { "processes_sampled": len(syscall_nodes), "network_processes": len(network_tracing), diff --git a/static/js/memory-belt.js b/static/js/memory-belt.js new file mode 100644 index 0000000..2dbb73b --- /dev/null +++ b/static/js/memory-belt.js @@ -0,0 +1,406 @@ +// Linux memory subsystem — strip map (same API payload as processes-realtime memory_visual) +// Version: 1 + +debugLog('💾 memory-belt.js v1: Script loading...'); + +class MemorySubsystemVisualization { + constructor() { + this.container = null; + this.canvas = null; + this.ctx = null; + this.exitButton = null; + this.isActive = false; + this.animationId = null; + this.telemetryInterval = null; + this.telemetry = null; + this.tick = 0; + this.memoryHoverStrip = null; + this.memoryStripHits = []; + this.memoryMapHit = null; + this.memoryHoverCell = null; + this.mouseMoveHandler = null; + } + + init(containerId = 'memory-belt-container') { + this.container = document.createElement('div'); + this.container.id = containerId; + this.container.style.cssText = ` + position: fixed; + inset: 0; + width: 100%; + height: 100%; + z-index: 9999; + overflow: hidden; + `; + document.body.appendChild(this.container); + + this.canvas = document.createElement('canvas'); + this.canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;'; + this.container.appendChild(this.canvas); + this.ctx = this.canvas.getContext('2d'); + this.mouseMoveHandler = (event) => this.onMouseMove(event); + this.canvas.addEventListener('mousemove', this.mouseMoveHandler); + this.onResize(); + window.addEventListener('resize', () => this.onResize()); + + this.exitButton = document.createElement('button'); + this.exitButton.textContent = 'BACK TO MAIN'; + this.exitButton.style.cssText = ` + position:absolute;top:18px;right:18px;padding:8px 14px;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:12px; cursor:pointer; + box-shadow: 0 0 14px rgba(150,175,220,0.25); + `; + this.exitButton.onclick = () => window.location.assign('/'); + this.container.appendChild(this.exitButton); + return true; + } + + onMouseMove(event) { + if (!this.canvas) return; + const rect = this.canvas.getBoundingClientRect(); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + this.memoryHoverStrip = null; + this.memoryHoverCell = null; + 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) { + this.memoryHoverStrip = hit; + this.canvas.style.cursor = 'crosshair'; + return; + } + } + } + this.canvas.style.cursor = 'default'; + } + + 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; + }) + .catch(() => { + this.telemetry = { + memory_visual: { + layout: 'strips', + rows: [], + summary: { + total_mb: 0, + used_percent: 0, + available_mb: 0, + swap_percent: 0, + source: 'fallback' + } + } + }; + }); + } + + drawPanel(x, y, w, h, title, opts = {}) { + const alpha = typeof opts.alpha === 'number' ? opts.alpha : 0.88; + const showTitle = opts.showTitle !== false; + const r = Math.max(0, Math.min(8, w / 2, h / 2)); + this.ctx.beginPath(); + this.ctx.moveTo(x + r, y); + this.ctx.lineTo(x + w - r, y); + this.ctx.quadraticCurveTo(x + w, y, x + w, y + r); + this.ctx.lineTo(x + w, y + h - r); + this.ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + this.ctx.lineTo(x + r, y + h); + this.ctx.quadraticCurveTo(x, y + h, x, y + h - r); + this.ctx.lineTo(x, y + r); + this.ctx.quadraticCurveTo(x, y, x + r, y); + this.ctx.closePath(); + this.ctx.fillStyle = `rgba(5, 9, 16, ${alpha})`; + this.ctx.fill(); + this.ctx.strokeStyle = 'rgba(140, 168, 210, 0.38)'; + this.ctx.lineWidth = 1; + this.ctx.stroke(); + if (showTitle && title) { + this.ctx.fillStyle = '#e8f0fc'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText(title, x + 12, y + 20); + } + } + + drawKernelHeader() { + const w = window.innerWidth; + this.ctx.textAlign = 'center'; + this.ctx.fillStyle = 'rgba(232, 240, 252, 0.92)'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText('linux kernel · memory management subsystem', w * 0.5, 26); + this.ctx.textAlign = 'start'; + } + + drawMemoryStats(x, y, w, h) { + const sum = this.telemetry?.memory_visual?.summary || {}; + 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'; + this.ctx.fillText(`total ${Number(sum.total_mb || 0).toFixed(0)} MiB`, x + 16, y + 46); + this.ctx.fillText(`used ${Number(sum.used_percent || 0).toFixed(1)}%`, x + 138, y + 46); + this.ctx.fillText(`avail ${Number(sum.available_mb || 0).toFixed(0)} MiB`, x + 238, y + 46); + this.ctx.fillText(`swap ${Number(sum.swap_percent || 0).toFixed(1)}%`, x + 388, y + 46); + this.ctx.fillText(`buf ${Number(sum.buffers_mb ?? 0).toFixed(0)} · cache ${Number(sum.cached_mb ?? 0).toFixed(0)} · anon ${Number(sum.anon_mb ?? 0).toFixed(0)} MiB`, x + 16, y + 64); + 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 + 80); + } + + tronHeatColor(t) { + const u = Math.max(0, Math.min(1, t)); + if (u < 0.42) { + const v = u / 0.42; + return `rgba(0, ${Math.floor(140 + 70 * v)}, ${Math.floor(160 + 95 * v)}, ${0.1 + v * 0.28})`; + } + const v = (u - 0.42) / 0.58; + return `rgba(${Math.floor(120 + 135 * v)}, ${Math.floor(255 - 20 * v)}, ${Math.floor(200 + 55 * v)}, ${0.32 + v * 0.48})`; + } + + tronHeatColorKind(kind, t) { + const u = Math.max(0, Math.min(1, t)); + const k = String(kind || 'anon'); + if (k === 'cached' || k === 'buffers' || k === 'mapped') { + return `rgba(${Math.floor(0 + 30 * u)}, ${Math.floor(165 + 90 * u)}, ${Math.floor(220)}, ${0.14 + u * 0.38})`; + } + if (k === 'slab' || k === 'kmeta') { + return `rgba(${Math.floor(80 + 60 * u)}, ${Math.floor(100 + 80 * u)}, ${Math.floor(240)}, ${0.16 + u * 0.42})`; + } + if (k === 'swap') { + return `rgba(${Math.floor(200 + 55 * u)}, ${Math.floor(80 + 40 * u)}, ${Math.floor(120 + 40 * u)}, ${0.22 + u * 0.45})`; + } + if (k === 'task') { + return this.tronHeatColor(u); + } + return this.tronHeatColor(u); + } + + drawMemorySidebarTron(sx, sy, sh, bars, label) { + this.ctx.fillStyle = 'rgba(0, 20, 28, 0.75)'; + this.ctx.fillRect(sx, sy, 40, sh); + this.ctx.strokeStyle = 'rgba(0, 229, 255, 0.35)'; + this.ctx.lineWidth = 1; + this.ctx.strokeRect(sx + 0.5, sy + 0.5, 39, sh - 1); + this.ctx.fillStyle = 'rgba(0, 229, 255, 0.65)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + this.ctx.fillText(label, sx + 4, sy + 10); + const n = Math.min(bars.length, 14); + for (let i = 0; i < n; i++) { + const bw = 4 + Math.floor(bars[i] * 28); + const by = sy + 10 + i * Math.floor((sh - 20) / n); + this.ctx.fillStyle = `rgba(0, 229, 255, ${0.15 + bars[i] * 0.5})`; + this.ctx.fillRect(sx + 6, by, bw, 3); + if (bars[i] > 0.55) { + this.ctx.fillStyle = `rgba(255, 252, 220, ${0.35 + bars[i] * 0.45})`; + this.ctx.fillRect(sx + 6, by, bw, 3); + } + } + } + + drawMemoryView(x, y, w, h) { + this.memoryMapHit = null; + this.memoryStripHits = []; + const mv = this.telemetry?.memory_visual; + const stripRows = Array.isArray(mv?.rows) ? mv.rows : []; + const titleH = 34; + this.drawPanel(x, y, w, titleH, 'memory topology strip · kernel accounting (not PFN map)', { alpha: 0.9 }); + + const side = 42; + const bottomH = 42; + const pad = 6; + const labelCol = 108; + const innerX = Math.floor(x + pad + side); + const innerY = Math.floor(y + titleH + pad); + const innerW = Math.floor(w - pad * 2 - side * 2); + const innerH = Math.floor(h - titleH - bottomH - pad * 2); + + this.ctx.strokeStyle = 'rgba(0, 229, 255, 0.48)'; + this.ctx.lineWidth = 1; + this.ctx.shadowColor = 'rgba(0, 229, 255, 0.35)'; + this.ctx.shadowBlur = 12; + this.ctx.strokeRect(innerX + 0.5, innerY + 0.5, innerW - 1, innerH - 1); + this.ctx.shadowBlur = 0; + this.ctx.strokeStyle = 'rgba(0, 229, 255, 0.18)'; + this.ctx.strokeRect(innerX + 2.5, innerY + 2.5, innerW - 5, innerH - 5); + + if (stripRows.length === 0) { + this.ctx.fillStyle = 'rgba(200, 230, 255, 0.7)'; + this.ctx.font = '11px "Share Tech Mono", monospace'; + this.ctx.fillText('no memory strip data — waiting for /api/processes-realtime', innerX + 12, innerY + 40); + return; + } + + const nrows = stripRows.length; + const gapY = 3; + const totalGap = gapY * Math.max(0, nrows - 1); + const rowAreaH = Math.max(16, Math.floor((innerH - totalGap) / Math.max(1, nrows))); + const stripX = innerX + labelCol + 4; + const stripW = Math.max(40, innerW - labelCol - 8); + + const barL = stripRows.map((row) => { + const bl = row.blocks || []; + if (!bl.length) return 0.2; + return bl.reduce((a, b) => a + Number(b.heat || 0), 0) / bl.length; + }); + const barR = stripRows.map((row) => { + const bl = row.blocks || []; + if (!bl.length) return 0.2; + return Math.max(...bl.map((b) => Number(b.heat || 0))); + }); + this.drawMemorySidebarTron(Math.floor(x + pad), innerY, innerH, barL, 'SEG_A'); + this.drawMemorySidebarTron(Math.floor(x + w - pad - 38), innerY, innerH, barR, 'SEG_B'); + + const gridDivs = 28; + for (let g = 0; g <= gridDivs; g++) { + const gx = stripX + (g / gridDivs) * stripW + 0.5; + this.ctx.beginPath(); + this.ctx.moveTo(gx, innerY); + this.ctx.lineTo(gx, innerY + innerH); + this.ctx.strokeStyle = g % 4 === 0 ? 'rgba(0, 229, 255, 0.14)' : 'rgba(0, 229, 255, 0.06)'; + this.ctx.lineWidth = 1; + this.ctx.stroke(); + } + + stripRows.forEach((row, ri) => { + const ry = innerY + ri * (rowAreaH + gapY); + const blocks = Array.isArray(row.blocks) ? row.blocks : []; + this.ctx.fillStyle = 'rgba(0, 229, 255, 0.42)'; + this.ctx.font = '8px "Share Tech Mono", monospace'; + const pct = row.pct_of_ram != null ? `${Number(row.pct_of_ram).toFixed(1)}%` : ''; + this.ctx.fillText(String(row.label || row.id || '').slice(0, 22), innerX + 4, ry + rowAreaH * 0.62); + this.ctx.fillStyle = 'rgba(0, 180, 200, 0.55)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + this.ctx.fillText(`${pct} · ${Number(row.kb || 0).toFixed(0)}k`, innerX + 4, ry + rowAreaH * 0.95); + + let cx = stripX; + blocks.forEach((blk, bi) => { + const bw = Number(blk.w || 0) * stripW; + const t = Number(blk.heat || 0); + const kind = blk.kind || row.id || 'anon'; + const px = Math.floor(cx); + const nx = Math.floor(cx + bw); + const pw = Math.max(1, nx - px); + cx += bw; + const ph = Math.max(4, Math.floor(rowAreaH) - 2); + const py = Math.floor(ry + 1); + this.ctx.fillStyle = this.tronHeatColorKind(kind, t); + this.ctx.fillRect(px, py, pw, ph); + if (t > 0.58) { + this.ctx.fillStyle = 'rgba(255, 255, 235, 0.28)'; + this.ctx.fillRect(px, py, pw, ph); + } + const greeble = ((ri * 131 + bi) * 7919) % 997; + if (pw > 10 && (greeble % 5 === 0)) { + this.ctx.strokeStyle = 'rgba(0, 229, 255, 0.35)'; + this.ctx.strokeRect(px + 1.5, py + 1.5, Math.min(pw - 3, 8), Math.min(ph - 3, 6)); + } + if (pw > 22) { + const hx = (greeble * 0x1000).toString(16).slice(0, 4); + this.ctx.fillStyle = 'rgba(200, 255, 255, 0.35)'; + this.ctx.font = '6px "Share Tech Mono", monospace'; + this.ctx.fillText(`0x${hx}`, px + 2, py + ph - 2); + } + if (kind === 'task' && blk.pid && pw > 36) { + this.ctx.fillStyle = 'rgba(255, 255, 240, 0.5)'; + this.ctx.font = '6px "Share Tech Mono", monospace'; + this.ctx.fillText(`${blk.name || blk.pid}`.slice(0, 8), px + 2, py + 8); + } + this.memoryStripHits.push({ + x: px, + y: py, + w: pw, + h: ph, + rowId: row.id, + kind, + blk, + label: row.label + }); + }); + + const ly = ry + rowAreaH + 0.5; + this.ctx.beginPath(); + this.ctx.moveTo(stripX, ly); + this.ctx.lineTo(innerX + innerW, ly); + this.ctx.strokeStyle = 'rgba(0, 229, 255, 0.11)'; + this.ctx.stroke(); + }); + + if (this.memoryHoverStrip) { + const hit = this.memoryHoverStrip; + this.ctx.strokeStyle = 'rgba(255, 248, 160, 0.9)'; + this.ctx.lineWidth = 1; + this.ctx.strokeRect(hit.x + 0.5, hit.y + 0.5, hit.w - 1, hit.h - 1); + let detail = `${hit.label || ''} · ${hit.kind}`; + if (hit.blk && hit.blk.pid) detail += ` · pid ${hit.blk.pid}`; + if (hit.blk && hit.blk.name) detail += ` ${hit.blk.name}`; + this.ctx.fillStyle = 'rgba(255, 252, 220, 0.95)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(detail.slice(0, 72), stripX, innerY + innerH - 6); + } + + this.ctx.fillStyle = 'rgba(0, 229, 255, 0.4)'; + this.ctx.font = '7px "Share Tech Mono", monospace'; + const base = innerY + innerH + 12; + for (let k = 0; k < 8; k++) { + const addr = 0xffff888000000000 + k * 0x2a00000; + this.ctx.fillText(`0x${addr.toString(16).slice(0, 12)}`, x + pad + k * (Math.min(140, w / 8.2)), base); + } + } + + drawScene() { + if (!this.ctx || !this.canvas) return; + const w = window.innerWidth; + const h = window.innerHeight; + this.ctx.clearRect(0, 0, w, h); + this.tick += 1; + + const gap = 16; + const top = 58; + const statsH = 98; + const graphY = top + statsH + gap; + const graphH = Math.max(260, h - graphY - 36); + + this.ctx.fillStyle = '#020305'; + this.ctx.fillRect(0, 0, w, h); + const gmem = this.ctx.createRadialGradient(w * 0.5, h * 0.45, 0, w * 0.5, h * 0.45, Math.max(w, h) * 0.65); + gmem.addColorStop(0, 'rgba(0, 45, 55, 0.35)'); + gmem.addColorStop(0.5, 'rgba(0, 12, 18, 0.92)'); + gmem.addColorStop(1, '#010203'); + this.ctx.fillStyle = gmem; + 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); + } + + animate() { + if (!this.isActive) return; + this.animationId = requestAnimationFrame(() => this.animate()); + this.drawScene(); + } + + activate() { + this.isActive = true; + this.fetchTelemetry(); + this.telemetryInterval = setInterval(() => { + if (this.isActive) this.fetchTelemetry(); + }, 1200); + this.animate(); + } + + onResize() { + if (!this.canvas || !this.ctx) return; + const dpr = Math.min(window.devicePixelRatio || 1, 2); + this.canvas.width = Math.floor(window.innerWidth * dpr); + this.canvas.height = Math.floor(window.innerHeight * dpr); + this.canvas.style.width = `${window.innerWidth}px`; + this.canvas.style.height = `${window.innerHeight}px`; + this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } +} + +window.MemorySubsystemVisualization = MemorySubsystemVisualization; +debugLog('💾 memory-belt.js: MemorySubsystemVisualization exported to window'); diff --git a/static/js/processes-belt.js b/static/js/processes-belt.js index 8178d5a..36bb2ae 100644 --- a/static/js/processes-belt.js +++ b/static/js/processes-belt.js @@ -1,7 +1,7 @@ // Processes Subsystem Visualization -// Version: 6 +// Version: 13 -debugLog('🧠 processes-belt.js v6: Script loading...'); +debugLog('🧠 processes-belt.js v13: Script loading...'); class ProcessesSubsystemVisualization { constructor() { @@ -52,7 +52,7 @@ class ProcessesSubsystemVisualization { this.exitButton = document.createElement('button'); this.exitButton.textContent = 'BACK TO MAIN'; this.exitButton.style.cssText = ` - position:absolute;top:20px;right:20px;padding:10px 18px;z-index:1001; + position:absolute;top:18px;right:18px;padding:8px 14px;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:12px; cursor:pointer; box-shadow: 0 0 14px rgba(150,175,220,0.25); @@ -69,10 +69,10 @@ class ProcessesSubsystemVisualization { createModeToggle() { const panel = document.createElement('div'); panel.style.cssText = ` - position:absolute;top:20px;left:20px;display:flex;gap:8px;z-index:1001; + position:absolute;top:18px;left:18px;display:flex;gap:8px;z-index:1001; `; const modes = [ - { key: 'temporal', label: 'TEMPORAL T-1/T/T+1' }, + { key: 'temporal', label: '3-LAYER GRAPH' }, { key: 'radial', label: 'RADIAL GRAPH' } ]; modes.forEach((m) => { @@ -106,7 +106,7 @@ class ProcessesSubsystemVisualization { createEdgeFilterToggle() { const panel = document.createElement('div'); panel.style.cssText = ` - position:absolute;top:56px;left:20px;display:flex;gap:6px;z-index:1001; + position:absolute;top:54px;left:18px;display:flex;flex-wrap:wrap;max-width:min(520px,92vw);gap:6px;z-index:1001; `; const filters = [ { key: 'all', label: 'ALL' }, @@ -177,7 +177,9 @@ class ProcessesSubsystemVisualization { }); } - drawPanel(x, y, w, h, title) { + drawPanel(x, y, w, h, title, opts = {}) { + const alpha = typeof opts.alpha === 'number' ? opts.alpha : 0.88; + const showTitle = opts.showTitle !== false; const r = Math.max(0, Math.min(8, w / 2, h / 2)); this.ctx.beginPath(); this.ctx.moveTo(x + r, y); @@ -190,103 +192,313 @@ class ProcessesSubsystemVisualization { this.ctx.lineTo(x, y + r); this.ctx.quadraticCurveTo(x, y, x + r, y); this.ctx.closePath(); - this.ctx.fillStyle = 'rgba(8, 11, 16, 0.88)'; + this.ctx.fillStyle = `rgba(5, 9, 16, ${alpha})`; this.ctx.fill(); - this.ctx.strokeStyle = 'rgba(165, 178, 200, 0.35)'; + this.ctx.strokeStyle = 'rgba(140, 168, 210, 0.38)'; this.ctx.lineWidth = 1; this.ctx.stroke(); - this.ctx.fillStyle = '#d8e5f7'; - this.ctx.font = '13px "Share Tech Mono", monospace'; - this.ctx.fillText(title, x + 14, y + 22); + if (showTitle && title) { + this.ctx.fillStyle = '#e8f0fc'; + this.ctx.font = '12px "Share Tech Mono", monospace'; + this.ctx.fillText(title, x + 12, y + 20); + } } - 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)'; + drawPerspectiveGrid(areaX, areaY, areaW, areaH) { + const vpX = areaX + areaW * 0.5; + const vpY = areaY - areaH * 1.35; + const rows = 16; + for (let i = 0; i <= rows; i++) { + const t = i / rows; + const yy = areaY + t * areaH; + const spread = 0.42 + t * 1.05; + 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.lineWidth = 1; + this.ctx.stroke(); + } + for (let i = -10; i <= 10; i++) { + const tx = 0.5 + i * 0.055; + const xb = areaX + areaW * tx; + 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.stroke(); + } + } + + drawSparklineInBox(bx, by, bw, bh, seed) { + this.ctx.fillStyle = 'rgba(0, 0, 0, 0.35)'; + this.ctx.fillRect(bx, by, bw, bh); + this.ctx.strokeStyle = 'rgba(57, 255, 120, 0.45)'; + this.ctx.strokeRect(bx, by, bw, bh); + this.ctx.strokeStyle = 'rgba(74, 222, 128, 0.92)'; + this.ctx.lineWidth = 1.25; + this.ctx.shadowColor = 'rgba(74, 255, 140, 0.55)'; + this.ctx.shadowBlur = 6; + this.ctx.beginPath(); + const steps = 20; + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const vx = bx + 2 + t * (bw - 4); + const wave = Math.sin(this.tick * 0.065 + seed * 1.7 + t * 9.2) + + 0.35 * Math.sin(this.tick * 0.11 + t * 14); + const vy = by + bh * 0.55 + wave * (bh * 0.32); + if (i === 0) this.ctx.moveTo(vx, vy); + else this.ctx.lineTo(vx, vy); + } + this.ctx.stroke(); + this.ctx.shadowBlur = 0; } - drawArrow(x1, y1, x2, y2, color, width = 1) { + drawLayerMeta(cx, topY, wBox, lines) { + const hBox = 12 + lines.length * 14; + const x0 = cx - wBox * 0.5; + this.ctx.fillStyle = 'rgba(4, 8, 14, 0.82)'; + this.ctx.strokeStyle = 'rgba(150, 178, 220, 0.4)'; + this.ctx.lineWidth = 1; + this.ctx.beginPath(); + const r = 4; + this.ctx.moveTo(x0 + r, topY); + this.ctx.lineTo(x0 + wBox - r, topY); + this.ctx.quadraticCurveTo(x0 + wBox, topY, x0 + wBox, topY + r); + this.ctx.lineTo(x0 + wBox, topY + hBox - r); + this.ctx.quadraticCurveTo(x0 + wBox, topY + hBox, x0 + wBox - r, topY + hBox); + this.ctx.lineTo(x0 + r, topY + hBox); + this.ctx.quadraticCurveTo(x0, topY + hBox, x0, topY + hBox - r); + this.ctx.lineTo(x0, topY + r); + this.ctx.quadraticCurveTo(x0, topY, x0 + r, topY); + this.ctx.closePath(); + this.ctx.fill(); + this.ctx.stroke(); + this.ctx.fillStyle = 'rgba(220, 232, 248, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + lines.forEach((line, i) => { + this.ctx.fillText(line, x0 + 8, topY + 16 + i * 14); + }); + } + + drawKernelHeader() { + const w = window.innerWidth; + this.ctx.textAlign = 'center'; + 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.textAlign = 'start'; + } + + quadraticControlForEdge(x1, y1, x2, y2, salt) { + const mx = (x1 + x2) * 0.5; + const my = (y1 + y2) * 0.5; const dx = x2 - x1; const dy = y2 - y1; - const len = Math.max(1, Math.sqrt(dx * dx + dy * dy)); - const ux = dx / len; - const uy = dy / len; - const ex = x2 - ux * 7; - const ey = y2 - uy * 7; + const len = Math.max(1e-6, Math.hypot(dx, dy)); + const nx = -dy / len; + const ny = dx / len; + const u = this.stableUnit(salt); + const sign = u >= 0.5 ? 1 : -1; + const wobble = (u - 0.5) * 0.55; + const mag = Math.min(130, len * 0.4) * (0.55 + Math.abs(u - 0.5) * 0.9); + return { + cx: mx + nx * mag * (sign + wobble), + cy: my + ny * mag * (sign + wobble) + }; + } + + quadBezierPoint(x0, y0, cx, cy, x1, y1, t) { + const omt = 1 - t; + return { + x: omt * omt * x0 + 2 * omt * t * cx + t * t * x1, + y: omt * omt * y0 + 2 * omt * t * cy + t * t * y1 + }; + } + + quadBezierTangent(x0, y0, cx, cy, x1, y1, t) { + const omt = 1 - t; + const tx = 2 * omt * (cx - x0) + 2 * t * (x1 - cx); + const ty = 2 * omt * (cy - y0) + 2 * t * (y1 - cy); + const l = Math.hypot(tx, ty) || 1; + return { tx: tx / l, ty: ty / l }; + } + + drawCurvedStroke(x1, y1, x2, y2, color, width, salt, withArrow, controlOverride) { + const { cx, cy } = controlOverride || this.quadraticControlForEdge(x1, y1, x2, y2, salt); this.ctx.beginPath(); this.ctx.moveTo(x1, y1); - this.ctx.lineTo(ex, ey); + this.ctx.quadraticCurveTo(cx, cy, x2, y2); this.ctx.strokeStyle = color; this.ctx.lineWidth = width; this.ctx.stroke(); - this.ctx.beginPath(); - this.ctx.moveTo(ex, ey); - this.ctx.lineTo(ex - uy * 3 - ux * 4, ey + ux * 3 - uy * 4); - this.ctx.lineTo(ex + uy * 3 - ux * 4, ey - ux * 3 - uy * 4); - this.ctx.closePath(); - this.ctx.fillStyle = color; - this.ctx.fill(); + if (withArrow) { + const tip = this.quadBezierPoint(x1, y1, cx, cy, x2, y2, 0.995); + const tan = this.quadBezierTangent(x1, y1, cx, cy, x2, y2, 0.98); + const ux = tan.tx; + const uy = tan.ty; + const back = 7; + const ex = tip.x - ux * back; + const ey = tip.y - uy * back; + this.ctx.beginPath(); + this.ctx.moveTo(tip.x, tip.y); + this.ctx.lineTo(ex - uy * 3 - ux * 4, ey + ux * 3 - uy * 4); + this.ctx.lineTo(ex + uy * 3 - ux * 4, ey - ux * 3 - uy * 4); + this.ctx.closePath(); + this.ctx.fillStyle = color; + this.ctx.fill(); + } + } + + radialCurveControl(sx, sy, tx, ty, centerX, centerY, salt) { + const mx = (sx + tx) * 0.5; + const my = (sy + ty) * 0.5; + const vx = mx - centerX; + const vy = my - centerY; + const len = Math.hypot(vx, vy) || 1; + const u = this.stableUnit(salt); + const bump = (u - 0.5) * 2; + const amp = Math.min(95, Math.hypot(tx - sx, ty - sy) * 0.42); + return { + cx: mx + (vx / len) * amp * (0.65 + bump * 0.35), + cy: my + (vy / len) * amp * (0.65 + bump * 0.35) + }; + } + + 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)'; + } + + stableUnit(n) { + const u = ((Number(n) * 1103515245 + 12345) >>> 0) % 10001; + return u / 10000; } drawNeuralGraph(x, y, w, h) { - this.drawPanel(x, y, w, h, 'PROCESS NEURAL GRAPH'); + this.drawPanel(x, y, w, 36, 'task graph · fork / wait / signals / IO', { alpha: 0.9 }); + const innerY = y + 42; + const innerH = h - 50; const graph = this.telemetry?.neural_graph || {}; - const nodes = Array.isArray(graph.nodes) ? graph.nodes.slice(0, 18) : []; - const rawEdges = Array.isArray(graph.edges) ? graph.edges.slice(0, 72) : []; + const nodes = Array.isArray(graph.nodes) ? graph.nodes.slice(0, 24) : []; + const rawEdges = Array.isArray(graph.edges) ? graph.edges.slice(0, 180) : []; const edges = rawEdges.filter((e) => this.edgeFilter === 'all' || String(e.type || '') === this.edgeFilter); 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, y + 56); + this.ctx.fillText('NO PROCESS GRAPH DATA', x + 20, innerY + 28); return; } - const pos = new Map(); - this.nodeHitAreas = []; + this.ctx.save(); + this.ctx.beginPath(); + this.ctx.rect(x + 2, innerY, w - 4, innerH); + this.ctx.clip(); + this.drawPerspectiveGrid(x + 4, innerY + innerH * 0.5, w - 8, innerH * 0.48); + this.ctx.restore(); + if (this.layoutMode === 'radial') { - this.drawRadialNeuralGraph(nodes, edges, pos, x, y, w, h); + const pos = new Map(); + this.drawRadialNeuralGraph(nodes, edges, pos, x, innerY, w, innerH); return; } - // Temporal layers to mimic T-1 / T / T+1 style. - const layerXs = [x + w * 0.24, x + w * 0.5, x + w * 0.76]; - const layerLabels = ['T-1', 'T', 'T+1']; - const layers = [[], [], []]; - nodes.forEach((node, idx) => layers[idx % 3].push(node)); + this.nodeHitAreas = []; + const leftX = x + w * 0.2; + const midX = x + w * 0.5; + const rightX = x + w * 0.8; + const panelY = innerY + 44; + const panelH = innerH - 56; + const panelW = Math.min(300, w * 0.29); + const metaW = Math.min(168, panelW - 8); + + this.drawPanel(leftX - panelW * 0.5, panelY, panelW, panelH, '', { alpha: 0.74, showTitle: false }); + this.drawPanel(midX - panelW * 0.5, panelY, panelW, panelH, '', { alpha: 0.74, showTitle: false }); + this.drawPanel(rightX - panelW * 0.5, panelY, panelW, panelH, '', { alpha: 0.74, showTitle: false }); + + const leftNodes = nodes.slice(0, 9); + const midNodes = nodes.slice(9, 18); + const rightNodes = nodes.slice(18, 24); + const outLabels = [ + 'RUNNING', 'SLEEP (S)', 'DISK (D)', 'STOPPED (T)', 'ZOMBIE', 'TRACE (t)', + 'RT BAND', 'CGROUP', 'IDLE', 'FORCED', 'IOWAIT', 'NICE+' + ]; - layerXs.forEach((lx, li) => { - this.ctx.strokeStyle = 'rgba(160, 176, 200, 0.15)'; - this.ctx.lineWidth = 1; - this.ctx.beginPath(); - this.ctx.moveTo(lx, y + 40); - this.ctx.lineTo(lx, y + h - 36); - this.ctx.stroke(); - this.ctx.fillStyle = 'rgba(200, 214, 232, 0.78)'; - this.ctx.font = '15px "Share Tech Mono", monospace'; - this.ctx.fillText(layerLabels[li], lx - 16, y + h - 12); - }); + this.drawLayerMeta(leftX, innerY + 8, metaW, [ + `tasks sampled: ${leftNodes.length}`, + 'metrics: cpu · rss · ctx · faults' + ]); + this.drawLayerMeta(midX, innerY + 8, metaW, [ + `related tasks: ${midNodes.length}`, + 'links: parent/child · fd · ipc' + ]); + this.drawLayerMeta(rightX, innerY + 8, metaW, [ + `task_state: ${rightNodes.length}`, + 'scheduler: CFS · runqueue' + ]); + + const leftPos = []; + const midPos = []; + const rightPos = []; + const positionByPid = new Map(); + const metricNames = [ + 'CPU %', 'RSS MiB', 'CTX/s', 'FAULT/s', 'OPEN FD', 'NICE', 'THREADS', + 'IO WAIT', 'CGROUP Q' + ]; - layers.forEach((layerNodes, li) => { - const lx = layerXs[li]; - const step = Math.max(30, (h - 120) / Math.max(1, layerNodes.length)); - const startY = y + 66 + Math.max(0, ((h - 130) - step * layerNodes.length) * 0.5); - layerNodes.forEach((node, idx) => { + const highlightIdx = Math.floor(this.tick / 55) % Math.max(1, leftNodes.length); + + const setLayer = (arr, colX, list, mode) => { + const step = panelH / (Math.max(1, list.length) + 1); + list.forEach((node, idx) => { + const yy = panelY + step * (idx + 1); const pid = Number(node.pid || 0); - const wave = Math.sin(this.tick * 0.02 + idx * 0.6 + li * 0.8) * 8; - const nx = lx + wave; - const ny = startY + idx * step; - pos.set(pid, { x: nx, y: ny, layer: li }); + let tag = ''; + if (mode === 'input') { + tag = `${metricNames[idx % metricNames.length]} · ${String(node.name || 'proc').slice(0, 8)}`; + } else if (mode === 'hidden') { + tag = `pid ${pid || idx + 1}`; + } else { + tag = outLabels[idx % outLabels.length]; + } + const point = { x: colX, y: yy, node, pid, tag, idx, mode }; + arr.push(point); + positionByPid.set(pid, point); }); - }); + }; + setLayer(leftPos, leftX, leftNodes, 'input'); + setLayer(midPos, midX, midNodes, 'hidden'); + setLayer(rightPos, rightX, rightNodes, 'output'); + + const drawDense = (from, to, alphaScale) => { + from.forEach((a, ai) => { + to.forEach((b, bi) => { + const pairSalt = ai * 7919 + bi * 104729; + const alpha = Math.min(0.34, (0.1 + this.stableUnit(pairSalt) * 0.16) * alphaScale); + this.ctx.globalAlpha = alpha; + this.drawCurvedStroke(a.x, a.y, b.x, b.y, 'rgba(228, 236, 252, 0.92)', 0.7, pairSalt, false); + this.ctx.globalAlpha = 1; + }); + }); + }; + drawDense(leftPos, midPos, 1.0); + drawDense(midPos, rightPos, 1.08); - // Track short history to show trails (T-2/T-1/T). - this.positionHistory.push(pos); - if (this.positionHistory.length > 3) this.positionHistory.shift(); + edges.forEach((edge, eidx) => { + const s = positionByPid.get(Number(edge.source || 0)); + const t = positionByPid.get(Number(edge.target || 0)); + if (!s || !t) return; + const color = this.edgeColor(String(edge.type || '')); + const isHoverEdge = this.hoveredNodePid && (s.pid === this.hoveredNodePid || t.pid === this.hoveredNodePid); + this.ctx.globalAlpha = isHoverEdge ? 0.92 : 0.52; + const salt = Number(edge.source || 0) * 31 + Number(edge.target || 0) * 17 + eidx * 13; + this.drawCurvedStroke(s.x, s.y, t.x, t.y, color, isHoverEdge ? 1.15 : 0.85, salt, true); + this.ctx.globalAlpha = 1; + }); - // Hover neighborhood detection. const neighborPids = new Set(); if (this.hoveredNodePid) { edges.forEach((edge) => { @@ -297,87 +509,87 @@ class ProcessesSubsystemVisualization { }); } - // Intra-layer dense links (neural cluster style) - layers.forEach((layerNodes) => { - for (let i = 0; i < layerNodes.length; i++) { - for (let j = i + 1; j < Math.min(layerNodes.length, i + 4); j++) { - const pa = pos.get(Number(layerNodes[i].pid || 0)); - const pb = pos.get(Number(layerNodes[j].pid || 0)); - if (!pa || !pb) continue; - this.ctx.beginPath(); - this.ctx.moveTo(pa.x, pa.y); - this.ctx.lineTo(pb.x, pb.y); - this.ctx.strokeStyle = 'rgba(196, 220, 245, 0.24)'; - this.ctx.lineWidth = 0.9; - this.ctx.stroke(); - } - } - }); + const sparkW = 52; + const sparkH = 18; + const labelLeftPad = leftX - panelW * 0.46; + const outputHighlightIdx = Math.floor(this.tick / 70) % Math.max(1, rightPos.length); - // Inter-layer long links from telemetry edges. - edges.forEach((edge, idx) => { - const s = pos.get(Number(edge.source || 0)); - const t = pos.get(Number(edge.target || 0)); - if (!s || !t) return; - const wv = Number(edge.weight || 0.4); - const color = this.edgeColor(String(edge.type || '')); - const isHoverEdge = this.hoveredNodePid && (Number(edge.source || 0) === this.hoveredNodePid || Number(edge.target || 0) === this.hoveredNodePid); - this.ctx.globalAlpha = isHoverEdge ? 0.95 : (0.2 + wv * 0.4); - this.drawArrow(s.x, s.y, t.x, t.y, color, 0.7 + wv * 1.1); - this.ctx.globalAlpha = 1; - const tpos = (this.tick * 0.006 + idx * 0.07) % 1; - const px = s.x + (t.x - s.x) * tpos; - const py = s.y + (t.y - s.y) * tpos; - this.ctx.beginPath(); - this.ctx.arc(px, py, 1.8, 0, Math.PI * 2); - this.ctx.fillStyle = color.replace('0.4', '0.95'); - this.ctx.fill(); - }); + const rowYellow = (p) => p.mode === 'input' && highlightIdx === p.idx; - nodes.forEach((node, idx) => { - const pid = Number(node.pid || 0); - const p = pos.get(pid); - if (!p) return; - const pressure = Number(node.syscall_pressure || 0); - const nodeR = 8 + Math.min(7, pressure / 20); - const danger = pressure >= 70; - const mode = String(node.seccomp_mode || 'unknown'); - const fill = danger ? '#eb7e7e' : (mode === 'filter' || mode === 'strict' ? '#60d69d' : '#8a9cea'); - const isHovered = this.hoveredNodePid && pid === this.hoveredNodePid; - const isNeighbor = this.hoveredNodePid && neighborPids.has(pid); + leftPos.forEach((p) => { + const rowHi = highlightIdx === p.idx && p.mode === 'input'; + if (rowHi) { + this.ctx.fillStyle = 'rgba(255, 214, 64, 0.34)'; + this.ctx.fillRect(labelLeftPad - 6, p.y - 14, leftX - labelLeftPad + sparkW + 36, 30); + } + this.ctx.fillStyle = 'rgba(232, 240, 252, 0.95)'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + this.ctx.textAlign = 'right'; + const shortLabel = p.tag.split('·')[0].trim(); + this.ctx.fillText(shortLabel, leftX - sparkW - 14, p.y + 3); + this.ctx.textAlign = 'start'; + this.drawSparklineInBox(leftX - sparkW - 6, p.y - 10, sparkW, sparkH, p.pid + p.idx); + const v = this.stableUnit(p.pid + this.tick * 0); + const pressure = Number(p.node.syscall_pressure || 0); + const val = (pressure / 100 * 0.4 + v * 0.6).toFixed(3); + this.ctx.fillStyle = 'rgba(200, 214, 232, 0.88)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText(val, leftX - sparkW - 6, p.y + 14); + }); - // Draw node trail ghosts. - for (let hi = 0; hi < this.positionHistory.length - 1; hi++) { - const histPos = this.positionHistory[hi].get(pid); - if (!histPos) continue; - const ghostAlpha = 0.08 + hi * 0.08; + const drawNeuron = (p, style) => { + const pressure = Number(p.node.syscall_pressure || 0); + const danger = pressure >= 72; + const isHovered = this.hoveredNodePid && p.pid === this.hoveredNodePid; + const isNeighbor = this.hoveredNodePid && neighborPids.has(p.pid); + const r = isHovered ? 8.5 : 6.5; + const fill = danger ? '#ff8a8a' : '#f4f8ff'; + + if (isHovered || rowYellow(p)) { + const g = this.ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, r * 3.2); + g.addColorStop(0, 'rgba(255, 255, 255, 0.45)'); + g.addColorStop(1, 'rgba(255, 255, 255, 0)'); this.ctx.beginPath(); - this.ctx.arc(histPos.x, histPos.y, Math.max(2, nodeR * 0.6), 0, Math.PI * 2); - this.ctx.fillStyle = fill; - this.ctx.globalAlpha = ghostAlpha; + this.ctx.arc(p.x, p.y, r * 3.2, 0, Math.PI * 2); + this.ctx.fillStyle = g; this.ctx.fill(); } - this.ctx.globalAlpha = 1; this.ctx.beginPath(); - this.ctx.arc(p.x, p.y, nodeR, 0, Math.PI * 2); + this.ctx.arc(p.x, p.y, r, 0, Math.PI * 2); this.ctx.fillStyle = fill; - this.ctx.globalAlpha = isHovered ? 1 : (isNeighbor ? 0.95 : 0.85); + this.ctx.globalAlpha = isHovered ? 1 : (isNeighbor ? 0.94 : 0.9); this.ctx.fill(); this.ctx.globalAlpha = 1; - this.ctx.strokeStyle = isHovered ? 'rgba(255,255,255,0.98)' : 'rgba(227, 241, 255, 0.95)'; - this.ctx.lineWidth = isHovered ? 2 : 1.1; + this.ctx.strokeStyle = isHovered ? '#fff6a8' : 'rgba(255, 255, 255, 0.92)'; + this.ctx.lineWidth = isHovered ? 2 : 1; this.ctx.stroke(); - this.ctx.fillStyle = '#d8e5f7'; - this.ctx.font = '9px "Share Tech Mono", monospace'; - this.ctx.fillText(`${String(node.name || 'proc').slice(0, 8)}`, p.x - 20, p.y + nodeR + 13); - this.nodeHitAreas.push({ x: p.x, y: p.y, r: nodeR, pid }); - }); - // Legend - this.ctx.fillStyle = '#a7b6cb'; - this.ctx.font = '10px "Share Tech Mono", monospace'; - this.ctx.fillText('hover node: highlight neighborhood | trails show T-2/T-1/T', x + 14, y + h - 14); + this.ctx.fillStyle = '#eaf1fb'; + this.ctx.font = '10px "Share Tech Mono", monospace'; + if (style === 'hidden') { + this.ctx.textAlign = 'right'; + this.ctx.fillText(p.tag, p.x - r - 6, p.y + 3); + this.ctx.textAlign = 'start'; + } else if (style === 'output') { + const hi = isHovered || (p.idx === outputHighlightIdx); + if (hi) { + this.ctx.fillStyle = 'rgba(255, 224, 70, 0.42)'; + this.ctx.fillRect(p.x + r + 4, p.y - 10, panelW * 0.42, 20); + } + this.ctx.fillStyle = '#f0f6ff'; + this.ctx.fillText(p.tag, p.x + r + 10, p.y + 3); + } + this.nodeHitAreas.push({ x: p.x, y: p.y, r, pid: p.pid }); + }; + + leftPos.forEach((p) => drawNeuron(p, 'input')); + midPos.forEach((p) => drawNeuron(p, 'hidden')); + rightPos.forEach((p) => drawNeuron(p, 'output')); + + this.ctx.fillStyle = 'rgba(140, 158, 188, 0.85)'; + this.ctx.font = '9px "Share Tech Mono", monospace'; + this.ctx.fillText('hover task: highlight syscall / ipc / net / vfs edges', x + 12, y + h - 10); } drawRadialNeuralGraph(nodes, edges, pos, x, y, w, h) { @@ -388,7 +600,7 @@ class ProcessesSubsystemVisualization { nodes.forEach((node, idx) => { const pid = Number(node.pid || 0); const a = ((Math.PI * 2) / Math.max(1, nodes.length)) * idx - Math.PI / 2; - const drift = Math.sin(this.tick * 0.02 + idx * 0.4) * 0.06; + const drift = Math.sin(this.tick * 0.0035 + idx * 0.4) * 0.018; const nx = cx + Math.cos(a + drift) * (ring * (0.75 + (idx % 4) * 0.07)); const ny = cy + Math.sin(a + drift) * (ring * (0.75 + (idx % 4) * 0.07)); pos.set(pid, { x: nx, y: ny }); @@ -424,14 +636,15 @@ class ProcessesSubsystemVisualization { const color = this.edgeColor(String(edge.type || '')); const isHoverEdge = this.hoveredNodePid && (Number(edge.source || 0) === this.hoveredNodePid || Number(edge.target || 0) === this.hoveredNodePid); this.ctx.globalAlpha = isHoverEdge ? 0.95 : (0.2 + wv * 0.38); - this.drawArrow(s.x, s.y, t.x, t.y, color, 0.7 + wv); + const salt = idx * 97 + Number(edge.source || 0) + Number(edge.target || 0); + const ctrl = this.radialCurveControl(s.x, s.y, t.x, t.y, cx, cy, salt); + this.drawCurvedStroke(s.x, s.y, t.x, t.y, color, 0.65 + wv, salt, true, ctrl); this.ctx.globalAlpha = 1; const tp = (this.tick * 0.006 + idx * 0.08) % 1; - const px = s.x + (t.x - s.x) * tp; - const py = s.y + (t.y - s.y) * tp; + const p = this.quadBezierPoint(s.x, s.y, ctrl.cx, ctrl.cy, t.x, t.y, tp); this.ctx.beginPath(); - this.ctx.arc(px, py, 1.7, 0, Math.PI * 2); - this.ctx.fillStyle = color.replace('0.4', '0.95'); + this.ctx.arc(p.x, p.y, 1.7, 0, Math.PI * 2); + this.ctx.fillStyle = 'rgba(240, 248, 255, 0.95)'; this.ctx.fill(); }); @@ -476,22 +689,22 @@ class ProcessesSubsystemVisualization { this.ctx.fillStyle = '#a7b6cb'; this.ctx.font = '10px "Share Tech Mono", monospace'; - this.ctx.fillText('radial mode: global coupling + hover neighborhood + short trails', x + 14, y + h - 14); + this.ctx.fillText('radial: task ring · curved edges · hover neighborhood', x + 14, y + h - 14); } drawTopStats(x, y, w, h) { - this.drawPanel(x, y, w, h, 'BEHAVIOR SIGNALS'); + this.drawPanel(x, y, w, h, 'runtime snapshot'); const meta = this.telemetry?.meta || {}; const graph = this.telemetry?.neural_graph || {}; const n = Array.isArray(graph.nodes) ? graph.nodes.length : 0; const e = Array.isArray(graph.edges) ? graph.edges.length : 0; this.ctx.fillStyle = '#d8e5f7'; this.ctx.font = '11px "Share Tech Mono", monospace'; - this.ctx.fillText(`nodes ${n}`, x + 16, y + 50); - this.ctx.fillText(`edges ${e}`, x + 130, y + 50); - this.ctx.fillText(`seccomp ${Number(meta.seccomp_filter_percent || 0).toFixed(2)}%`, x + 232, y + 50); + this.ctx.fillText(`tasks ${n}`, x + 16, y + 50); + this.ctx.fillText(`relations ${e}`, x + 118, y + 50); + this.ctx.fillText(`seccomp filter ${Number(meta.seccomp_filter_percent || 0).toFixed(1)}%`, x + 238, y + 50); this.ctx.fillStyle = '#a7b6cb'; - this.ctx.fillText('processes as neural behavior graph: nodes=processes, edges=syscalls/ipc/network/file', x + 16, y + 72); + this.ctx.fillText('vertices: processes · edges: syscalls · ipc · sockets · vfs', x + 16, y + 72); } drawScene() { @@ -501,25 +714,20 @@ class ProcessesSubsystemVisualization { this.ctx.clearRect(0, 0, w, h); this.tick += 1; - const bg = this.ctx.createRadialGradient(w * 0.5, h * 0.4, 0, w * 0.5, h * 0.4, Math.max(w, h) * 0.72); - bg.addColorStop(0, '#121821'); - bg.addColorStop(0.7, '#0a0d12'); - bg.addColorStop(1, '#0a0d12'); - this.ctx.fillStyle = bg; - this.ctx.fillRect(0, 0, w, h); - - this.ctx.fillStyle = '#d4dbe8'; - this.ctx.font = '24px "Share Tech Mono", monospace'; - this.ctx.fillText('KERNEL PROCESSES SUBSYSTEM', w * 0.5 - 220, 42); - this.ctx.fillStyle = '#a7b6cb'; - this.ctx.font = '11px "Share Tech Mono", monospace'; - this.ctx.fillText('neural behavior model: processes as nodes, kernel interactions as edges', w * 0.5 - 290, 64); - const gap = 16; - const top = 86; + const top = 58; const statsH = 98; const graphY = top + statsH + gap; - const graphH = Math.max(260, h - graphY - 20); + 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'); + this.ctx.fillStyle = bg; + this.ctx.fillRect(0, 0, w, h); + + this.drawKernelHeader(); this.drawTopStats(gap, top, w - gap * 2, statsH); this.drawNeuralGraph(gap, graphY, w - gap * 2, graphH); } diff --git a/static/js/right-semicircle-menu.js b/static/js/right-semicircle-menu.js index c7dc887..9070a44 100644 --- a/static/js/right-semicircle-menu.js +++ b/static/js/right-semicircle-menu.js @@ -33,6 +33,7 @@ class RightSemicircleMenuManager { || itemId === 'files' || itemId === 'scheduler' || itemId === 'processes' + || itemId === 'memory' || itemId === 'security'; } @@ -93,6 +94,10 @@ class RightSemicircleMenuManager { window.location.assign('/linux-processes-subsystem.html'); return; } + if (itemId === 'memory') { + window.location.assign('/linux-memory-subsystem'); + return; + } if (!window.kernelContextMenu) return; if (itemId === 'kernel') { window.kernelContextMenu.activateDNAView(); @@ -363,6 +368,10 @@ class RightSemicircleMenuManager { window.location.assign('/linux-processes-subsystem.html'); return; } + if (item.id === 'memory') { + window.location.assign('/linux-memory-subsystem'); + return; + } if (item.id === 'security') { window.location.assign('/linux-security-subsystem'); return; @@ -391,6 +400,10 @@ class RightSemicircleMenuManager { window.location.assign('/linux-processes-subsystem.html'); return; } + if (item.id === 'memory') { + window.location.assign('/linux-memory-subsystem'); + return; + } if (item.id === 'security') { window.location.assign('/linux-security-subsystem'); return; diff --git a/templates/linux-memory-subsystem.html b/templates/linux-memory-subsystem.html new file mode 100644 index 0000000..29c2526 --- /dev/null +++ b/templates/linux-memory-subsystem.html @@ -0,0 +1,109 @@ + + +
+ + ++ This page visualizes memory accounting from Linux kernel sources such as /proc/meminfo + and sampled process RSS, shown as horizontal strips and block topology. +
+