diff --git a/app.py b/app.py index 406ae8b..b3f7756 100644 --- a/app.py +++ b/app.py @@ -287,110 +287,181 @@ def get_system_info(): 395: 'futex_wake_requeue_pi' } +# Max PIDs to scan for /proc/[pid]/syscall (tasks currently blocked in a syscall). +KERNEL_DNA_MAX_PROCS = int(os.environ.get('KERNEL_DNA_MAX_PROCS', '1200')) + + +def _kernel_dna_read_proc_vmstat(): + """Parse /proc/vmstat into a dict of int counters.""" + vm = {} + try: + with open('/proc/vmstat', 'r', encoding='utf-8', errors='replace') as f: + for line in f: + parts = line.split() + if len(parts) >= 2: + vm[parts[0]] = int(parts[1]) + except (OSError, ValueError): + pass + return vm + + +def _kernel_dna_vmstat_activity_nucleotides(): + """Real VM counters when no per-task syscall sample is available.""" + result = [] + vm = _kernel_dna_read_proc_vmstat() + mapping = [ + ('pgfault', 'mm'), + ('pgmajfault', 'mm'), + ('pswpin', 'mm'), + ('pswpout', 'mm'), + ('oom_kill', 'mm'), + ('nr_dirty', 'mm'), + ('nr_written', 'mm'), + ('pgscan_kswapd', 'mm'), + ('pgscan_direct', 'mm'), + ('workingset_refault', 'mm'), + ] + for key, sub in mapping: + if key in vm and vm[key] > 0: + result.append({'name': f'vm:{key}', 'count': vm[key], 'subsystem': sub}) + return result + + +def _kernel_dna_block_device_activity_nucleotides(): + """Cumulative I/O from /sys/block//stat.""" + result = [] + tr = tw = tsr = tsw = 0 + try: + for name in os.listdir('/sys/block'): + if name.startswith(('loop', 'ram')): + continue + stat_path = os.path.join('/sys/block', name, 'stat') + if not os.path.isfile(stat_path): + continue + with open(stat_path, 'r', encoding='utf-8', errors='replace') as f: + st = f.read().split() + if len(st) < 7: + continue + tr += int(st[0]) + tsr += int(st[2]) + tw += int(st[4]) + tsw += int(st[6]) + except (OSError, ValueError, IndexError): + pass + if tr > 0: + result.append({'name': 'disk:read_ios', 'count': tr, 'subsystem': 'fs'}) + if tw > 0: + result.append({'name': 'disk:write_ios', 'count': tw, 'subsystem': 'fs'}) + if tsr > 0: + result.append({'name': 'disk:sectors_read', 'count': tsr, 'subsystem': 'fs'}) + if tsw > 0: + result.append({'name': 'disk:sectors_written', 'count': tsw, 'subsystem': 'fs'}) + return result + + +def _kernel_dna_sockstat_activity_nucleotides(): + """Socket counts from /proc/net/sockstat.""" + result = [] + try: + with open('/proc/net/sockstat', 'r', encoding='utf-8', errors='replace') as f: + for line in f: + parts = line.split() + if line.startswith('TCP:') and len(parts) >= 3: + result.append({'name': 'net:tcp_inuse', 'count': int(parts[2]), 'subsystem': 'net'}) + elif line.startswith('UDP:') and len(parts) >= 3: + result.append({'name': 'net:udp_inuse', 'count': int(parts[2]), 'subsystem': 'net'}) + except (OSError, ValueError, IndexError): + pass + return result + + +def _kernel_dna_softirq_nucleotides(limit=8): + """Per-vector softirq totals from /proc/softirqs.""" + out = [] + try: + with open('/proc/softirqs', 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + if len(lines) < 2: + return out + for line in lines[1 : 1 + limit]: + parts = line.split() + if len(parts) < 2: + continue + vec = parts[0].rstrip(':') + total = sum(int(x) for x in parts[1:] if x.isdigit()) + if total > 0: + out.append({ + 'type': 'interrupt', + 'code': 'T', + 'name': f'softirq:{vec}', + 'count': total, + 'subsystem': map_interrupt_to_subsystem(vec), + 'timestamp': datetime.now().isoformat(), + }) + except (OSError, ValueError): + pass + return out + + def get_real_system_calls(): - """Get real system calls from /proc filesystem""" + """Blocked-in-syscall sample from /proc/[pid]/syscall; else real vmstat + block + sockstat (no random on Linux).""" try: if platform.system() != 'Linux': return get_mock_system_calls() - - # Dictionary to count syscalls - syscall_counts = {} - - # Read /proc/*/syscall for all processes - # This shows the current syscall each process is executing - proc_dirs = [] + try: proc_dirs = [d for d in os.listdir('/proc') if d.isdigit()] except PermissionError: - # If we can't read /proc, use limited access - pass - - # Sample up to 100 processes to avoid performance issues - sampled_procs = proc_dirs[:100] if len(proc_dirs) > 100 else proc_dirs - - for pid in sampled_procs: + proc_dirs = [] + + sampled = sorted(proc_dirs, key=int)[: min(KERNEL_DNA_MAX_PROCS, len(proc_dirs))] + + syscall_counts = {} + for pid in sampled: try: syscall_path = f'/proc/{pid}/syscall' - if os.path.exists(syscall_path): - with open(syscall_path, 'r') as f: - line = f.read().strip() - if line and line != '-1': - # Format: syscall_number arg1 arg2 ... (or just number) - parts = line.split() - if parts: - try: - syscall_num = int(parts[0]) - syscall_name = SYSCALL_NAMES.get(syscall_num, f'syscall_{syscall_num}') - syscall_counts[syscall_name] = syscall_counts.get(syscall_name, 0) + 1 - except ValueError: - continue - except (PermissionError, FileNotFoundError, IOError): - # Process may have terminated or we don't have permission + if not os.path.exists(syscall_path): + continue + with open(syscall_path, 'r', encoding='utf-8', errors='replace') as f: + line = f.read().strip() + if not line or line in ('-1', 'running'): + continue + parts = line.split() + if not parts: + continue + try: + syscall_num = int(parts[0]) + except ValueError: + continue + syscall_name = SYSCALL_NAMES.get(syscall_num, f'syscall_{syscall_num}') + syscall_counts[syscall_name] = syscall_counts.get(syscall_name, 0) + 1 + except (PermissionError, FileNotFoundError, IOError, ValueError): continue - - # Also get CPU statistics from /proc/stat which includes context switches - try: - with open('/proc/stat', 'r') as f: - stat_lines = f.readlines() - for line in stat_lines: - if line.startswith('ctxt '): - # Context switches indicate syscall activity - ctxt_switches = int(line.split()[1]) - # Use this to scale our counts - break - except (IOError, ValueError, IndexError): - ctxt_switches = 0 - - # Convert counts to list format expected by frontend - syscalls = [] + if syscall_counts: - # Sort by count and take top 10 - sorted_syscalls = sorted(syscall_counts.items(), key=lambda x: x[1], reverse=True)[:10] - - for name, count in sorted_syscalls: - # Format: "count total" where total is a larger number - # This matches the frontend expectation - total = count * 1000 + random.randint(100, 999) # Add some variation - formatted_count = f"{count:03d} {total:06d}" - syscalls.append({'name': name, 'count': formatted_count}) - else: - # Fallback: use /proc/stat to infer activity - try: - with open('/proc/stat', 'r') as f: - cpu_line = f.readline() - if cpu_line.startswith('cpu '): - # CPU stats include user and system time - parts = cpu_line.split() - if len(parts) >= 4: - user_time = int(parts[1]) - system_time = int(parts[3]) - # Estimate syscall activity from system time - activity_level = system_time % 1000 - - # Common syscalls that are likely active - common_syscalls = ['read', 'write', 'open', 'close', 'mmap', - 'fork', 'execve', 'socket', 'connect', 'accept'] - - for i, name in enumerate(common_syscalls[:10]): - # Use activity level to create realistic counts - count = (activity_level + i * 10) % 999 + 1 - total = count * 1000 + random.randint(100, 999) - formatted_count = f"{count:03d} {total:06d}" - syscalls.append({'name': name, 'count': formatted_count}) - except (IOError, ValueError, IndexError): - pass - - # If we still don't have data, use mock - if not syscalls: - return get_mock_system_calls() - - return syscalls - + syscalls = [] + for name, count in sorted(syscall_counts.items(), key=lambda x: x[1], reverse=True)[:20]: + syscalls.append({ + 'name': name, + 'count': count, + 'subsystem': map_syscall_to_subsystem(name), + }) + return syscalls + + merged = [] + merged.extend(_kernel_dna_vmstat_activity_nucleotides()) + merged.extend(_kernel_dna_block_device_activity_nucleotides()) + merged.extend(_kernel_dna_sockstat_activity_nucleotides()) + if merged: + merged.sort(key=lambda x: x['count'], reverse=True) + return merged[:20] + return [] + except Exception as e: print(f"Error getting system calls: {e}") import traceback traceback.print_exc() - return get_mock_system_calls() + return [] if platform.system() == 'Linux' else get_mock_system_calls() def get_mock_system_calls(): """Mock data for system calls""" @@ -750,6 +821,22 @@ def processes_page_legacy(): """Legacy path redirect to Linux processes subsystem page.""" return redirect('/linux-processes-subsystem', code=301) + +@app.route('/linux-crypto-subsystem.html') +def linux_crypto_subsystem_html(): + return redirect('/linux-crypto-subsystem', code=301) + + +@app.route('/linux-security-subsystem.html') +def linux_security_subsystem_html(): + return redirect('/linux-security-subsystem', code=301) + + +@app.route('/linux-processes-subsystem.html') +def linux_processes_subsystem_html(): + return redirect('/linux-processes-subsystem', code=301) + + @app.route('/linux-memory-subsystem') def linux_memory_subsystem_page(): """SEO-friendly Linux memory subsystem page.""" @@ -2684,14 +2771,11 @@ def get_proc_timeline(): # Get process info proc_info = proc.as_dict(['pid', 'name', 'create_time', 'status']) - - # Event: exec (process creation) - timeline.append({ - 'type': 'exec', - 'timestamp': datetime.fromtimestamp(proc_info['create_time']).isoformat(), - 'pid': pid - }) - + base_ts = float(proc_info['create_time']) + # Ordered real-derived events; timestamps are monotonic from process start (precise times not in /proc). + ordered_events = [] + ordered_events.append({'type': 'exec', 'pid': pid}) + # Event: mmap (from /proc/[pid]/maps) try: maps_path = f'/proc/{pid}/maps' @@ -2699,15 +2783,14 @@ def get_proc_timeline(): with open(maps_path, 'r') as f: map_count = len(f.readlines()) if map_count > 0: - timeline.append({ + ordered_events.append({ 'type': 'mmap', - 'timestamp': datetime.now().isoformat(), 'pid': pid, 'count': map_count }) except (IOError, PermissionError): pass - + # Event: read/write (from /proc/[pid]/io) try: io_path = f'/proc/{pid}/io' @@ -2718,61 +2801,54 @@ def get_proc_timeline(): if ':' in line: key, value = line.split(':', 1) io_data[key.strip()] = int(value.strip()) - + if io_data.get('read_bytes', 0) > 0: - timeline.append({ + ordered_events.append({ 'type': 'read', - 'timestamp': datetime.now().isoformat(), 'pid': pid, 'bytes': io_data.get('read_bytes', 0) }) - + if io_data.get('write_bytes', 0) > 0: - timeline.append({ + ordered_events.append({ 'type': 'write', - 'timestamp': datetime.now().isoformat(), 'pid': pid, 'bytes': io_data.get('write_bytes', 0) }) except (IOError, PermissionError): pass - + # Event: connect/accept (from /proc/[pid]/net/tcp) try: tcp_path = f'/proc/{pid}/net/tcp' if os.path.exists(tcp_path): with open(tcp_path, 'r') as f: lines = f.readlines() - if len(lines) > 1: # Has connections (excluding header) - # Check connection states - for line in lines[1:]: # Skip header + if len(lines) > 1: + for line in lines[1:]: parts = line.split() if len(parts) >= 4: state = parts[3] - # State 01 = ESTABLISHED (connect), 0A = LISTEN (accept) if state == '01': - timeline.append({ - 'type': 'connect', - 'timestamp': datetime.now().isoformat(), - 'pid': pid - }) + ordered_events.append({'type': 'connect', 'pid': pid}) elif state == '0A': - timeline.append({ - 'type': 'accept', - 'timestamp': datetime.now().isoformat(), - 'pid': pid - }) + ordered_events.append({'type': 'accept', 'pid': pid}) except (IOError, PermissionError): pass - - # Sort timeline by timestamp - timeline.sort(key=lambda x: x['timestamp']) + + step = 0.35 + timeline = [] + for i, ev in enumerate(ordered_events): + ev = dict(ev) + ev['timestamp'] = datetime.fromtimestamp(base_ts + i * step).isoformat() + timeline.append(ev) return jsonify({ 'timeline': timeline, 'pid': pid, 'name': proc_info.get('name', 'unknown'), - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now().isoformat(), + 'timeline_time_basis': 'Events are ordered from process start; 0.35s steps separate rows for the helix (kernel does not expose per-event wall times for these signals).', }) except Exception as e: @@ -3091,8 +3167,8 @@ def get_kernel_dna_data(): 'type': 'syscall', 'code': 'A', 'name': syscall['name'], - 'count': syscall.get('count', '0'), - 'subsystem': map_syscall_to_subsystem(syscall['name']), + 'count': syscall.get('count', 0), + 'subsystem': syscall.get('subsystem') or map_syscall_to_subsystem(syscall['name']), 'timestamp': datetime.now().isoformat() }) except Exception as e: @@ -3119,16 +3195,7 @@ def get_kernel_dna_data(): }) except (IOError, ValueError, PermissionError) as e: print(f"Error collecting interrupts: {e}") - # Fallback: generate some sample interrupts - for irq_name in ['timer', 'keyboard', 'mouse', 'network']: - dna_data['nucleotides'].append({ - 'type': 'interrupt', - 'code': 'T', - 'name': irq_name, - 'count': random.randint(100, 1000), - 'subsystem': map_interrupt_to_subsystem(irq_name), - 'timestamp': datetime.now().isoformat() - }) + dna_data['nucleotides'].extend(_kernel_dna_softirq_nucleotides()) # 3. Collect context switches (C nucleotides) try: @@ -3227,6 +3294,14 @@ def get_kernel_dna_data(): def map_syscall_to_subsystem(syscall_name): """Map syscall name to kernel subsystem""" + if not syscall_name: + return 'kernel' + if syscall_name.startswith('vm:'): + return 'mm' + if syscall_name.startswith('disk:'): + return 'fs' + if syscall_name.startswith('net:'): + return 'net' syscall_lower = syscall_name.lower() if any(x in syscall_lower for x in ['read', 'write', 'open', 'close', 'stat', 'fsync']): return 'fs' @@ -4522,10 +4597,39 @@ def _build_memory_visual_rows(meminfo_kb, syscall_nodes, vm, swap): ("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)), ] + # Slab: split reclaimable vs unreclaimable when both exist (Linux 2.6.19+). + if mi.get("SReclaimable") is not None and mi.get("SUnreclaim") is not None: + row_specs.append(("sreclaim", "slab reclaimable", mi.get("SReclaimable", 0))) + row_specs.append(("sunreclaim", "slab unreclaimable", mi.get("SUnreclaim", 0))) + else: + row_specs.append(("slab", "slab / kmalloc", mi.get("Slab", 0))) + row_specs.extend( + [ + ("shmem", "shmem / tmpfs", mi.get("Shmem", 0)), + ("mapped", "file mappings", mi.get("Mapped", 0)), + ] + ) + dirty_wb = int(mi.get("Dirty", 0) or 0) + int(mi.get("Writeback", 0) or 0) + int( + mi.get("WritebackTmp", 0) or 0 + ) + if dirty_wb > 0: + row_specs.append(("dirty_wb", "dirty + writeback", dirty_wb)) + ah = int(mi.get("AnonHugePages", 0) or 0) + if ah > 0: + row_specs.append(("anon_huge", "transparent huge pages (anon)", ah)) + shm_h = int(mi.get("ShmemHugePages", 0) or 0) + if shm_h > 0: + row_specs.append(("shmem_huge", "huge pages (shmem)", shm_h)) + vmu = int(mi.get("VmallocUsed", 0) or 0) + if vmu > 0: + row_specs.append(("vmalloc", "vmalloc used", vmu)) + ac = int(mi.get("Active", 0) or 0) + iac = int(mi.get("Inactive", 0) or 0) + if ac > 0: + row_specs.append(("active", "active (LRU)", ac)) + if iac > 0: + row_specs.append(("inactive", "inactive (LRU)", iac)) 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) @@ -4598,6 +4702,11 @@ def _build_memory_visual_rows(meminfo_kb, syscall_nodes, vm, swap): } ) + dirty_kb = int(mi.get("Dirty", 0) or 0) + wb_kb = int(mi.get("Writeback", 0) or 0) + sr_kb = int(mi.get("SReclaimable", 0) or 0) + su_kb = int(mi.get("SUnreclaim", 0) or 0) + slab_total_kb = int(mi.get("Slab", 0) or 0) or (sr_kb + su_kb) summary = { "total_mb": round(mt / 1024.0, 1), "used_percent": round(vm.percent, 1) if vm else 0.0, @@ -4606,9 +4715,19 @@ def _build_memory_visual_rows(meminfo_kb, syscall_nodes, vm, swap): "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), + "slab_mb": round(slab_total_kb / 1024.0, 1), + "sreclaimable_mb": round(sr_kb / 1024.0, 1), + "sunreclaim_mb": round(su_kb / 1024.0, 1), + "dirty_mb": round(dirty_kb / 1024.0, 2), + "writeback_mb": round(wb_kb / 1024.0, 2), + "dirty_writeback_mb": round(dirty_wb / 1024.0, 2), + "anon_huge_mb": round(ah / 1024.0, 2), + "shmem_huge_mb": round(shm_h / 1024.0, 2), + "vmalloc_mb": round(vmu / 1024.0, 2), + "active_mb": round(ac / 1024.0, 1), + "inactive_mb": round(iac / 1024.0, 1), "swap_used_mb": round(swap_used / 1024.0, 1) if swap_tot else 0.0, - "source": "proc_meminfo+psutil", + "source": "proc_meminfo+psutil+v2", } return rows, summary diff --git a/index.html b/index.html index a000bb4..994a1c8 100755 --- a/index.html +++ b/index.html @@ -96,14 +96,14 @@

Linux Kernel Ring 0 Visualization

- - + + - + - + diff --git a/static/js/kernel-context-menu.js b/static/js/kernel-context-menu.js index 223b547..77d1dc2 100644 --- a/static/js/kernel-context-menu.js +++ b/static/js/kernel-context-menu.js @@ -1,12 +1,12 @@ // Kernel Context Menu - Submenu and View Modes -// Version: 13 +// Version: 15 debugLog('🔧 kernel-context-menu.js v13: Script loading...'); class KernelContextMenu { constructor() { this.isVisible = false; - this.currentView = null; // 'matrix', 'timeline', 'dna', or null + this.currentView = null; // 'matrix', 'timeline', 'dna', 'kernel-flow', or null this.selectedPid = null; this.matrixData = []; this.timelineData = []; @@ -21,178 +21,12 @@ class KernelContextMenu { } init() { - // Submenu will be created dynamically in right-semicircle-menu.js - } - - showSubmenu(x, y, angle) { - const svg = d3.select('svg'); - - // Remove existing submenu - this.hideSubmenu(); - - // Calculate submenu position (to the left of Kernel item) - const submenuX = x - 150; - const submenuY = y; - - // Create submenu group - this.submenuGroup = svg.append('g') - .attr('class', 'kernel-submenu') - .style('opacity', 0) - .style('pointer-events', 'all'); - - debugLog('✅ Submenu group created'); - - // Background - diegetic UI style panel (like "SUBJECT U454.1" from example) - const bg = this.submenuGroup.append('rect') - .attr('x', submenuX - 10) - .attr('y', submenuY - 80) - .attr('width', 140) - .attr('height', 110) - .attr('rx', 2) - .style('fill', 'rgba(5, 8, 12, 0.85)') // Very dark background, slightly transparent - .style('stroke', 'rgba(200, 200, 200, 0.15)') // Subtle light gray border - .style('stroke-width', '0.5px') - .style('pointer-events', 'all') - .style('filter', 'drop-shadow(0 0 2px rgba(200, 200, 200, 0.1))'); // Subtle glow - - debugLog('✅ Background rect created at:', submenuX - 10, submenuY - 60); - - // Menu items - const items = [ - { id: 'matrix', label: 'Matrix View' }, - { id: 'timeline', label: 'Timeline / Flow' }, - { id: 'filters', label: 'Filters / Settings' } - ]; - - items.forEach((item, i) => { - const itemY = submenuY - 65 + (i * 25); - const isActive = item.id === this.currentView; - const baseColor = '#c8ccd4'; // milk-gray - const accentColor = '#58b6d8'; // cold cyan accent - const itemGroup = this.submenuGroup.append('g') - .attr('class', `submenu-item submenu-${item.id}`) - .style('cursor', 'pointer'); - - // Create individual panel for each item (like "SUBJECT U454.1" style) - const itemPanel = itemGroup.append('rect') - .attr('x', submenuX - 8) - .attr('y', itemY - 9) - .attr('width', 136) - .attr('height', 20) - .attr('rx', 8) // More rounded corners like in example - .style('fill', '#333') // Same base color as right menu panels - .style('stroke', '#555') // Same border color as right menu panels - .style('stroke-width', '1px') - .style('pointer-events', 'all'); - // No opacity - same as right menu panels (fully opaque) - - // Text - positioned inside the panel - const text = itemGroup.append('text') - .attr('x', submenuX) - .attr('y', itemY + 2) - .text(item.label.toUpperCase()) - .style('font-family', 'Share Tech Mono, monospace') - .style('font-size', '10px') - .style('fill', isActive ? accentColor : baseColor) - .style('pointer-events', 'none') - .style('letter-spacing', '0.5px'); // Slight letter spacing for clarity - - // Hover handlers for the panel - const handleMouseEnter = () => { - itemPanel - .style('fill', '#ffffff') - .style('stroke', '#ffffff') - .style('opacity', 1); - // On hover, use dark text (like right menu) - text.style('fill', isActive ? accentColor : '#000000'); - }; - - const handleMouseLeave = () => { - itemPanel - .style('fill', '#333') - .style('stroke', '#555'); - // No opacity - same as right menu panels (fully opaque) - text.style('fill', isActive ? accentColor : baseColor); - }; - - // Click handler function - const handleClick = () => { - if (item.id === 'matrix') { - this.activateMatrixView(); - } else if (item.id === 'timeline') { - // Activate DNA Timeline mode (with or without PID) - if (this.dnaVisualization) { - this.dnaVisualization.activateTimelineMode(this.selectedPid || null); - this.currentView = 'dna-timeline'; - this.hideSubmenu(); - - // Hide other UI elements - d3.selectAll('.syscall-box, .syscall-text').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); - d3.selectAll('.tag-icon, .connection-line').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); - d3.selectAll('.connection-box, .connection-text, .connection-details').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); - d3.selectAll('.subsystem-indicator').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); - - // Stop auto-updates - if (window.syscallsManager) { - window.syscallsManager.stopAutoUpdate(); - } - if (window.connectionsManager) { - window.connectionsManager.stopAutoUpdate(); - } - - // Timeline DNA view uses its own internal EXIT button. - } else { - // Fallback to regular timeline if DNA visualization not available - if (this.selectedPid) { - this.activateTimelineView(); - } else { - alert('Please select a PID from Matrix View first'); - } - } - } else if (item.id === 'filters') { - // Placeholder for future - debugLog('Filters/Settings - coming soon'); - } - }; - - // Add hover and click handlers to panel - itemPanel - .on('mouseenter', handleMouseEnter) - .on('mouseleave', handleMouseLeave) - .on('click', handleClick); - - // Also add hover and click handlers to text area for better UX - const textHoverArea = itemGroup.append('rect') - .attr('x', submenuX - 8) - .attr('y', itemY - 9) - .attr('width', 136) - .attr('height', 20) - .style('fill', 'transparent') - .style('pointer-events', 'all') - .on('mouseenter', handleMouseEnter) - .on('mouseleave', handleMouseLeave) - .on('click', handleClick); - }); - - // Animate appearance - this.submenuGroup.transition() - .duration(200) - .style('opacity', 1) - .on('end', () => { - debugLog('✅ Submenu animation completed'); - }); - - debugLog('✅ Submenu created and animated'); + // Kernel submenu removed; DNA view includes process timeline UI. } hideSubmenu() { - debugLog('🔒 hideSubmenu called'); - if (this.submenuGroup) { - this.submenuGroup.remove(); - this.submenuGroup = null; - } - // Also remove by class in case group reference is lost d3.selectAll('.kernel-submenu').remove(); + this.submenuGroup = null; } activateMatrixView() { @@ -280,6 +114,191 @@ class KernelContextMenu { this.startAutoUpdate(); } + /** + * Full-screen diagram: how data moves userspace → socket → TCP → kernel net stack → NIC. + * Educational / illustrative ordering (real paths vary by workload). + */ + activateKernelFlowMode() { + debugLog('🌊 activateKernelFlowMode'); + this.currentView = 'kernel-flow'; + this.hideSubmenu(); + + d3.selectAll('.kernel-flow-layer, .kernel-flow-backdrop').remove(); + + d3.selectAll('.process-line, .process-circle, .process-name') + .transition() + .duration(300) + .style('opacity', 0.15); + + d3.selectAll('.syscall-box, .syscall-text') + .transition() + .duration(300) + .style('opacity', 0) + .style('pointer-events', 'none') + .style('visibility', 'hidden'); + + d3.selectAll('.connection-box, .connection-text, .connection-details') + .transition() + .duration(300) + .style('opacity', 0) + .style('pointer-events', 'none'); + + d3.selectAll('.tag-icon, .connection-line') + .transition() + .duration(300) + .style('opacity', 0) + .style('pointer-events', 'none') + .style('visibility', 'hidden'); + + d3.selectAll('.subsystem-indicator') + .transition() + .duration(300) + .style('opacity', 0) + .style('pointer-events', 'none') + .style('visibility', 'hidden'); + + if (window.syscallsManager) { + window.syscallsManager.stopAutoUpdate(); + } + if (window.connectionsManager) { + window.connectionsManager.stopAutoUpdate(); + } + + d3.selectAll('.bezier-curve') + .transition() + .duration(400) + .attr('opacity', 0.12); + + this.renderKernelFlowDiagram(); + this.addExitButton(); + } + + renderKernelFlowDiagram() { + const svg = d3.select('svg'); + const width = window.innerWidth; + const height = window.innerHeight; + + const backdrop = svg.append('rect') + .attr('class', 'kernel-flow-backdrop') + .attr('x', 0) + .attr('y', 0) + .attr('width', width) + .attr('height', height) + .attr('fill', 'rgba(2, 3, 6, 0.72)') + .style('pointer-events', 'all'); + + const g = svg.append('g').attr('class', 'kernel-flow-layer'); + + const defs = g.append('defs'); + defs.append('marker') + .attr('id', 'kernel-flow-arrowhead') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 8) + .attr('refY', 0) + .attr('markerWidth', 5) + .attr('markerHeight', 5) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', 'rgba(88, 182, 216, 0.75)'); + + const steps = [ + { main: 'nginx', sub: 'userspace' }, + { main: 'socket', sub: 'fd · buffers' }, + { main: 'TCP', sub: 'sk_buff' }, + { main: 'kernel', sub: 'net stack' }, + { main: 'NIC', sub: 'driver → DMA' } + ]; + + const nodeW = Math.min(118, Math.max(88, (width - 160) / 6.2)); + const gap = Math.min(32, Math.max(14, (width - 160 - steps.length * nodeW) / (steps.length - 1))); + const totalW = steps.length * nodeW + (steps.length - 1) * gap; + const startX = (width - totalW) / 2; + const cy = height * 0.44; + + g.append('text') + .attr('x', width / 2) + .attr('y', cy - 72) + .attr('text-anchor', 'middle') + .style('font-family', 'Share Tech Mono, monospace') + .style('font-size', '13px') + .style('fill', 'rgba(200, 210, 225, 0.92)') + .style('letter-spacing', '2px') + .text('KERNEL FLOW MODE'); + + g.append('text') + .attr('x', width / 2) + .attr('y', cy - 48) + .attr('text-anchor', 'middle') + .style('font-family', 'Share Tech Mono, monospace') + .style('font-size', '9px') + .style('fill', 'rgba(120, 140, 160, 0.75)') + .text('nginx → socket → TCP → kernel → NIC · illustrative TX/RX path'); + + steps.forEach((step, i) => { + const x = startX + i * (nodeW + gap); + const node = g.append('g').attr('transform', `translate(${x},${cy})`); + + node.append('rect') + .attr('width', nodeW) + .attr('height', 52) + .attr('rx', 6) + .attr('fill', 'rgba(12, 18, 28, 0.92)') + .attr('stroke', 'rgba(88, 182, 216, 0.45)') + .attr('stroke-width', 1); + + node.append('text') + .attr('x', nodeW / 2) + .attr('y', 22) + .attr('text-anchor', 'middle') + .style('font-family', 'Share Tech Mono, monospace') + .style('font-size', '12px') + .style('fill', '#e8eef8') + .text(step.main); + + node.append('text') + .attr('x', nodeW / 2) + .attr('y', 40) + .attr('text-anchor', 'middle') + .style('font-family', 'Share Tech Mono, monospace') + .style('font-size', '8px') + .style('fill', 'rgba(140, 160, 185, 0.85)') + .text(step.sub); + + if (i < steps.length - 1) { + const x1 = x + nodeW + 4; + const x2 = x + nodeW + gap - 4; + g.append('line') + .attr('x1', x1) + .attr('y1', cy + 26) + .attr('x2', x2) + .attr('y2', cy + 26) + .attr('stroke', 'rgba(88, 182, 216, 0.55)') + .attr('stroke-width', 1.2) + .attr('marker-end', 'url(#kernel-flow-arrowhead)'); + } + }); + + g.append('text') + .attr('x', width / 2) + .attr('y', cy + 88) + .attr('text-anchor', 'middle') + .style('font-family', 'Share Tech Mono, monospace') + .style('font-size', '8px') + .style('fill', 'rgba(90, 105, 125, 0.8)') + .text('Ordering is simplified; buffers, softirq, and qdisc can reorder work in real kernels.'); + + g.style('opacity', 0) + .transition() + .duration(350) + .style('opacity', 1); + + backdrop.style('opacity', 0) + .transition() + .duration(300) + .style('opacity', 1); + } + activateDNAView() { debugLog('🧬 Activating Kernel DNA View'); debugLog('🔍 KernelDNAVisualization available:', typeof KernelDNAVisualization); @@ -346,6 +365,13 @@ class KernelContextMenu { } else { debugLog('✅ Using existing dnaVisualization instance'); } + + if (this.dnaVisualization) { + this.dnaVisualization.timelineMode = false; + this.dnaVisualization.selectedPid = null; + this.dnaVisualization.timeStart = null; + this.dnaVisualization.currentTimelineHeight = 0; + } // Hide other UI elements d3.selectAll('.syscall-box, .syscall-text').style('opacity', 0).style('pointer-events', 'none').style('visibility', 'hidden'); @@ -792,7 +818,8 @@ class KernelContextMenu { .transition() .duration(300) .style('opacity', 1) - .style('pointer-events', 'all'); + .style('pointer-events', 'all') + .style('visibility', 'visible'); // Restore active connections blocks d3.selectAll('.connection-box, .connection-text, .connection-details') @@ -838,6 +865,8 @@ class KernelContextMenu { // Clear Timeline events d3.selectAll('.timeline-event').remove(); + + d3.selectAll('.kernel-flow-layer, .kernel-flow-backdrop').remove(); // Restore Bezier curves - ensure all curves are visible with original styles d3.selectAll('.bezier-curve') diff --git a/static/js/kernel-dna.js b/static/js/kernel-dna.js index 15199d1..847990d 100644 --- a/static/js/kernel-dna.js +++ b/static/js/kernel-dna.js @@ -1,10 +1,10 @@ // Kernel DNA Visualization - Double Helix Structure // Represents Linux kernel execution paths as DNA strands -// Version: 19 +// Version: 21 — UX: enter/exit transitions, loading skeleton, staggered UI reveal -debugLog('🧬 kernel-dna.js v19: Script loading...'); -debugLog('🧬 kernel-dna.js v19: THREE available:', typeof THREE); -debugLog('🧬 kernel-dna.js v19: Browser:', navigator.userAgent); +debugLog('🧬 kernel-dna.js v21: Script loading...'); +debugLog('🧬 kernel-dna.js v21: THREE available:', typeof THREE); +debugLog('🧬 kernel-dna.js v21: Browser:', navigator.userAgent); class KernelDNAVisualization { constructor() { @@ -37,6 +37,8 @@ class KernelDNAVisualization { this.mouseMoveHandler = null; // Store mouse move handler reference this.hoveredNucleotide = null; // Track hovered nucleotide for yellow highlight this.exitButton = null; // Store exit button reference + this._loadingOverlay = null; + this._uxStylesInjected = false; // Color palette - New design system this.colors = { @@ -214,6 +216,9 @@ class KernelDNAVisualization { // Add exit button this.addExitButton(); + this.container.classList.add('kernel-dna-ux'); + this._ensureUxStyles(); + // Handle window resize window.addEventListener('resize', () => this.onWindowResize()); @@ -455,6 +460,141 @@ class KernelDNAVisualization { } } + _ensureUxStyles() { + if (this._uxStylesInjected || typeof document === 'undefined') return; + const style = document.createElement('style'); + style.id = 'kernel-dna-ux-styles'; + style.textContent = ` + #kernel-dna-container.kernel-dna-ux { + transition: opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.42s cubic-bezier(0.22, 1, 0.36, 1); + } + #kernel-dna-container .dna-loading-overlay { + position: absolute; + inset: 0; + z-index: 10050; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + background: rgba(14, 17, 20, 0.92); + backdrop-filter: blur(6px); + pointer-events: none; + transition: opacity 0.32s ease; + } + #kernel-dna-container .dna-loading-overlay.dna-loading-out { + opacity: 0; + } + #kernel-dna-container .dna-loading-label { + font-family: 'Share Tech Mono', monospace; + font-size: 11px; + letter-spacing: 0.35px; + color: rgba(200, 204, 212, 0.75); + } + #kernel-dna-container .dna-skeleton-wrap { + width: min(280px, 70vw); + display: flex; + flex-direction: column; + gap: 10px; + } + #kernel-dna-container .dna-skeleton-bar { + height: 8px; + border-radius: 4px; + background: linear-gradient(90deg, + rgba(90, 98, 108, 0.25) 0%, + rgba(130, 140, 155, 0.45) 50%, + rgba(90, 98, 108, 0.25) 100%); + background-size: 200% 100%; + animation: kernel-dna-skel 1.1s ease-in-out infinite; + } + #kernel-dna-container .dna-skeleton-bar:nth-child(2) { width: 88%; } + #kernel-dna-container .dna-skeleton-bar:nth-child(3) { width: 72%; } + @keyframes kernel-dna-skel { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + } + `; + document.head.appendChild(style); + this._uxStylesInjected = true; + } + + _showLoadingOverlay(labelText = 'Loading kernel view') { + if (!this.container) return; + this._hideLoadingOverlayImmediate(); + const ov = document.createElement('div'); + ov.className = 'dna-loading-overlay'; + ov.setAttribute('aria-busy', 'true'); + ov.setAttribute('aria-label', labelText); + window.setSafeHtml(ov, ` +
+
+
+
+
+
${labelText}
+ `); + this.container.appendChild(ov); + this._loadingOverlay = ov; + } + + _hideLoadingOverlayImmediate() { + if (this._loadingOverlay && this._loadingOverlay.parentNode) { + this._loadingOverlay.parentNode.removeChild(this._loadingOverlay); + } + this._loadingOverlay = null; + } + + _hideLoadingOverlay() { + return new Promise((resolve) => { + const ov = this._loadingOverlay; + if (!ov) { + resolve(); + return; + } + const done = () => { + this._hideLoadingOverlayImmediate(); + resolve(); + }; + ov.classList.add('dna-loading-out'); + const t = window.setTimeout(done, 340); + ov.addEventListener('transitionend', () => { + window.clearTimeout(t); + done(); + }, { once: true }); + }); + } + + _applyStaggeredReveal(nodes) { + const valid = nodes.filter(Boolean); + if (valid.length === 0) return; + const step = 88; + const ease = '0.42s cubic-bezier(0.22, 1, 0.36, 1)'; + valid.forEach((el) => { + el.style.transition = `opacity ${ease}, transform ${ease}`; + el.style.opacity = '0'; + if (el.classList.contains('dna-title') || el.classList.contains('dna-timeline-subtitle') || el.classList.contains('dna-dev-label')) { + el.style.transform = 'translate(-50%, 12px)'; + } else { + el.style.transform = 'translateY(12px)'; + } + }); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + valid.forEach((el, i) => { + window.setTimeout(() => { + el.style.opacity = '1'; + if (el.classList.contains('dna-title') || el.classList.contains('dna-timeline-subtitle') || el.classList.contains('dna-dev-label')) { + el.style.transform = 'translate(-50%, 0)'; + } else { + el.style.transform = 'translateY(0)'; + } + }, i * step); + }); + }); + }); + } + async render() { if (!this.isActive) return; @@ -476,10 +616,14 @@ class KernelDNAVisualization { // Clear previous visualization this.clear(); + this._showLoadingOverlay('Loading kernel DNA'); // Load data const data = await this.loadData(); - if (!data) return; + if (!data) { + await this._hideLoadingOverlay(); + return; + } // Create helix strands const leftHelix = this.createHelixStrand(true); @@ -544,8 +688,10 @@ class KernelDNAVisualization { }); } - // Add labels - this.addLabels(data); + await this._hideLoadingOverlay(); + + // Add labels + SELECT PROCESS (same panel as former DNA Timeline mode) + await this.addLabels(data); // Start animation only if not already animating if (!this.isAnimating) { @@ -556,9 +702,13 @@ class KernelDNAVisualization { async renderTimeline() { if (!this.selectedPid) { - // Show process selector if no PID selected + this._showLoadingOverlay('Loading processes'); this.clear(); - this.addTimelineLabels(null); + try { + await this.addTimelineLabels(null); + } finally { + await this._hideLoadingOverlay(); + } return; } @@ -572,6 +722,7 @@ class KernelDNAVisualization { savedRightRotation = this.helixRight.rotation.y; } + this._showLoadingOverlay('Loading timeline'); // Load timeline data try { const response = await fetch(`/api/proc-timeline?pid=${this.selectedPid}`); @@ -679,7 +830,7 @@ class KernelDNAVisualization { this.addTimelineMarkers(leftHelix.curve); // Update labels for timeline mode - this.addTimelineLabels(data); + await this.addTimelineLabels(data); // Start animation if (!this.isAnimating) { @@ -688,6 +839,8 @@ class KernelDNAVisualization { } } catch (error) { console.error('❌ Error rendering timeline:', error); + } finally { + await this._hideLoadingOverlay(); } } @@ -731,15 +884,15 @@ class KernelDNAVisualization { this.helixLeft.add(markerGroup); } - addTimelineLabels(data) { + async addTimelineLabels(data) { // Remove old labels - const oldLabels = this.container.querySelectorAll('.dna-title, .dna-legend, .dna-timeline-info, .dna-process-selector'); + const oldLabels = this.container.querySelectorAll('.dna-title, .dna-timeline-subtitle, .dna-legend, .dna-dev-label, .dna-timeline-info, .dna-process-selector'); oldLabels.forEach(label => label.remove()); // Add title const titleDiv = document.createElement('div'); titleDiv.className = 'dna-title'; - titleDiv.textContent = 'KERNEL DNA TIMELINE'; + titleDiv.textContent = 'KERNEL DNA'; titleDiv.style.cssText = ` position: absolute; top: 20px; @@ -751,10 +904,26 @@ class KernelDNAVisualization { z-index: 1001; `; this.container.appendChild(titleDiv); - this.appendInDevelopmentLabel(52); + const sub = document.createElement('div'); + sub.className = 'dna-timeline-subtitle'; + sub.textContent = 'single-process timeline'; + sub.style.cssText = ` + position: absolute; + top: 72px; + left: 50%; + transform: translateX(-50%); + color: rgba(88, 182, 216, 0.85); + font-family: 'Share Tech Mono', monospace; + font-size: 11px; + letter-spacing: 0.4px; + z-index: 1001; + `; + this.container.appendChild(sub); + const devLabel = this.appendInDevelopmentLabel(52); // Add process selector - this.addProcessSelector(); + await this.addProcessSelector(); + const selectorEl = this.container.querySelector('.dna-process-selector'); // Add timeline info const infoDiv = document.createElement('div'); @@ -779,6 +948,8 @@ class KernelDNAVisualization { z-index: 1001; `; this.container.appendChild(infoDiv); + + this._applyStaggeredReveal([titleDiv, sub, devLabel, selectorEl, infoDiv]); } async addProcessSelector() { @@ -800,10 +971,13 @@ class KernelDNAVisualization { font-family: 'Share Tech Mono', monospace; `; - // Add header + // Add header + optional clear (back to full kernel DNA view) const header = document.createElement('div'); - header.textContent = 'SELECT PROCESS'; header.style.cssText = ` + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; color: #c8ccd4; font-size: 12px; font-weight: bold; @@ -811,6 +985,32 @@ class KernelDNAVisualization { padding-bottom: 8px; border-bottom: 1px solid rgba(160, 170, 190, 0.2); `; + const headerTitle = document.createElement('span'); + headerTitle.textContent = 'SELECT PROCESS'; + header.appendChild(headerTitle); + if (this.selectedPid) { + const clearBtn = document.createElement('button'); + clearBtn.type = 'button'; + clearBtn.textContent = 'Show all kernel'; + clearBtn.style.cssText = ` + background: rgba(5, 8, 12, 0.8); + border: 1px solid rgba(88, 182, 216, 0.45); + color: #58b6d8; + font-family: 'Share Tech Mono', monospace; + font-size: 10px; + padding: 4px 8px; + border-radius: 3px; + cursor: pointer; + `; + clearBtn.onclick = async () => { + this.timelineMode = false; + this.selectedPid = null; + this.timeStart = null; + this.currentTimelineHeight = 0; + await this.render(); + }; + header.appendChild(clearBtn); + } selectorDiv.appendChild(header); // Add search input @@ -888,11 +1088,12 @@ class KernelDNAVisualization { procItem.style.borderColor = 'rgba(160, 170, 190, 0.15)'; } }; - procItem.onclick = () => { + procItem.onclick = async () => { this.selectedPid = proc.pid; - this.timeStart = null; // Reset timeline + this.timelineMode = true; + this.timeStart = null; this.currentTimelineHeight = 0; - this.renderTimeline(); + await this.renderTimeline(); }; processList.appendChild(procItem); @@ -933,9 +1134,9 @@ class KernelDNAVisualization { } } - addLabels(data) { + async addLabels(data) { // Remove old labels first - const oldLabels = this.container.querySelectorAll('.dna-title, .dna-legend'); + const oldLabels = this.container.querySelectorAll('.dna-title, .dna-legend, .dna-dev-label, .dna-process-selector'); oldLabels.forEach(label => label.remove()); // Add title @@ -953,7 +1154,7 @@ class KernelDNAVisualization { z-index: 1001; `; this.container.appendChild(titleDiv); - this.appendInDevelopmentLabel(52); + const devLabel = this.appendInDevelopmentLabel(52); // Add legend - Diegetic UI style const legendDiv = document.createElement('div'); @@ -974,6 +1175,10 @@ class KernelDNAVisualization { z-index: 1001; `; this.container.appendChild(legendDiv); + + await this.addProcessSelector(); + const selectorEl = this.container.querySelector('.dna-process-selector'); + this._applyStaggeredReveal([titleDiv, devLabel, legendDiv, selectorEl]); } appendInDevelopmentLabel(topPx = 52) { @@ -997,6 +1202,7 @@ class KernelDNAVisualization { text-transform: lowercase; `; this.container.appendChild(devLabel); + return devLabel; } animate() { @@ -1097,7 +1303,7 @@ class KernelDNAVisualization { } // Remove labels (but keep exit button) - const labels = this.container.querySelectorAll('.dna-title, .dna-legend, .dna-timeline-info, .dna-process-selector'); + const labels = this.container.querySelectorAll('.dna-title, .dna-timeline-subtitle, .dna-legend, .dna-dev-label, .dna-timeline-info, .dna-process-selector'); labels.forEach(label => label.remove()); } @@ -1107,6 +1313,7 @@ class KernelDNAVisualization { debugLog('🔍 Container element:', this.container); this.isActive = true; + this._ensureUxStyles(); // Ensure container exists and is visible if (!this.container) { @@ -1118,6 +1325,15 @@ class KernelDNAVisualization { debugLog('✅ Setting container display to block'); this.container.style.display = 'block'; this.container.style.zIndex = '9999'; + this.container.style.opacity = '0'; + this.container.style.transform = 'translateY(14px)'; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (!this.container || !this.isActive) return; + this.container.style.opacity = '1'; + this.container.style.transform = 'translateY(0)'; + }); + }); debugLog('✅ Container display:', this.container.style.display); debugLog('✅ Container z-index:', this.container.style.zIndex); const computed = window.getComputedStyle(this.container); @@ -1199,11 +1415,40 @@ class KernelDNAVisualization { } }); - if (this.container) { - this.container.style.display = 'none'; + const finalizeHide = () => { + this._hideLoadingOverlayImmediate(); + if (this.container) { + this.container.style.display = 'none'; + this.container.style.pointerEvents = ''; + this.container.style.opacity = ''; + this.container.style.transform = ''; + } + this.clear(); + }; + + if (this.container && this.container.style.display !== 'none') { + this.container.style.pointerEvents = 'none'; + let finalized = false; + const runFinalize = () => { + if (finalized) return; + finalized = true; + window.clearTimeout(fallbackTimer); + if (this.container) { + this.container.removeEventListener('transitionend', onEnd); + } + finalizeHide(); + }; + const onEnd = (e) => { + if (e && e.propertyName && e.propertyName !== 'opacity' && e.propertyName !== 'transform') return; + runFinalize(); + }; + this.container.addEventListener('transitionend', onEnd); + this.container.style.opacity = '0'; + this.container.style.transform = 'translateY(12px)'; + const fallbackTimer = window.setTimeout(runFinalize, 480); + } else { + finalizeHide(); } - - this.clear(); } onWindowResize() { diff --git a/static/js/main.js b/static/js/main.js index 4276e2a..2c63d7c 100755 --- a/static/js/main.js +++ b/static/js/main.js @@ -158,8 +158,11 @@ function draw() { const mobileLayout = isMobileLayout(); // Skip drawing if Matrix View is active to prevent elements from appearing above it - if (!mobileLayout && window.kernelContextMenu && window.kernelContextMenu.currentView === 'matrix') { - debugLog('⏸️ Skipping draw() - Matrix View is active'); + if (!mobileLayout && window.kernelContextMenu && ( + window.kernelContextMenu.currentView === 'matrix' || + window.kernelContextMenu.currentView === 'kernel-flow' + )) { + debugLog('⏸️ Skipping draw() - Matrix or Kernel Flow view is active'); return; } @@ -274,7 +277,8 @@ function draw() { // Use setTimeout to ensure this happens after all other rendering // But skip if Matrix View is active setTimeout(() => { - if (syscallsManager && (!window.kernelContextMenu || window.kernelContextMenu.currentView !== 'matrix')) { + const cv = window.kernelContextMenu && window.kernelContextMenu.currentView; + if (syscallsManager && cv !== 'matrix' && cv !== 'kernel-flow') { // Force update to ensure system calls are displayed syscallsManager.updateSyscallsTable(); } @@ -655,8 +659,11 @@ function getPointOnPathAtDistance(pathData, targetDistance, centerX, centerY) { // Draw tag icons function drawTagIcons(centerX, centerY) { // Skip drawing tag icons if Matrix View is active - if (window.kernelContextMenu && window.kernelContextMenu.currentView === 'matrix') { - debugLog('⏸️ Skipping tag icons render - Matrix View is active'); + if (window.kernelContextMenu && ( + window.kernelContextMenu.currentView === 'matrix' || + window.kernelContextMenu.currentView === 'kernel-flow' + )) { + debugLog('⏸️ Skipping tag icons render - Matrix or Kernel Flow view is active'); return; } diff --git a/static/js/memory-belt.js b/static/js/memory-belt.js index 2dbb73b..fcd90c2 100644 --- a/static/js/memory-belt.js +++ b/static/js/memory-belt.js @@ -1,5 +1,5 @@ // Linux memory subsystem — strip map (same API payload as processes-realtime memory_visual) -// Version: 1 +// Version: 2 — deeper meminfo: slab split, dirty/writeback, THP, vmalloc, LRU debugLog('💾 memory-belt.js v1: Script loading...'); @@ -145,9 +145,29 @@ class MemorySubsystemVisualization { 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); + const sr = Number(sum.sreclaimable_mb ?? 0); + const su = Number(sum.sunreclaim_mb ?? 0); + const slabLine = sr > 0 || su > 0 + ? `slab ${Number(sum.slab_mb ?? 0).toFixed(0)} MiB (recl ${sr.toFixed(0)} · unrecl ${su.toFixed(0)})` + : `slab ${Number(sum.slab_mb ?? 0).toFixed(0)} MiB`; + this.ctx.fillText(slabLine, x + 16, y + 80); + const dw = Number(sum.dirty_writeback_mb ?? 0); + const dirty = Number(sum.dirty_mb ?? 0); + const wb = Number(sum.writeback_mb ?? 0); + const line3 = [ + dw > 0 ? `dirty+wb ${dw.toFixed(2)} MiB (d ${dirty.toFixed(2)} · wb ${wb.toFixed(2)})` : null, + Number(sum.anon_huge_mb ?? 0) > 0 ? `THP anon ${Number(sum.anon_huge_mb).toFixed(2)} MiB` : null, + Number(sum.shmem_huge_mb ?? 0) > 0 ? `huge shmem ${Number(sum.shmem_huge_mb).toFixed(2)} MiB` : null, + Number(sum.vmalloc_mb ?? 0) > 0 ? `vmalloc ${Number(sum.vmalloc_mb).toFixed(1)} MiB` : null, + Number(sum.active_mb ?? 0) > 0 ? `LRU act ${Number(sum.active_mb).toFixed(0)}` : null, + Number(sum.inactive_mb ?? 0) > 0 ? `inact ${Number(sum.inactive_mb).toFixed(0)} MiB` : null, + ].filter(Boolean).join(' · '); + if (line3) { + this.ctx.fillText(line3.slice(0, 118), x + 16, y + 96); + } 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); + this.ctx.fillText('strips ≈ meminfo buckets; task row = RSS share of sampled PIDs; not a physical PFN map', x + 16, y + 112); } tronHeatColor(t) { @@ -166,7 +186,19 @@ class MemorySubsystemVisualization { 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') { + if (k === 'dirty_wb') { + return `rgba(${Math.floor(255 * u)}, ${Math.floor(120 + 80 * u)}, ${Math.floor(40 + 40 * u)}, ${0.22 + u * 0.42})`; + } + if (k === 'anon_huge' || k === 'shmem_huge') { + return `rgba(${Math.floor(60 + 100 * u)}, ${Math.floor(220)}, ${Math.floor(140 + 60 * u)}, ${0.18 + u * 0.4})`; + } + if (k === 'vmalloc') { + return `rgba(${Math.floor(180 + 50 * u)}, ${Math.floor(80 + 100 * u)}, ${Math.floor(255)}, ${0.2 + u * 0.38})`; + } + if (k === 'active' || k === 'inactive') { + return `rgba(${Math.floor(40 + 80 * u)}, ${Math.floor(200 + 40 * u)}, ${Math.floor(255)}, ${0.15 + u * 0.35})`; + } + if (k === 'slab' || k === 'sreclaim' || k === 'sunreclaim' || 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') { @@ -211,7 +243,7 @@ class MemorySubsystemVisualization { const side = 42; const bottomH = 42; const pad = 6; - const labelCol = 108; + const labelCol = 132; const innerX = Math.floor(x + pad + side); const innerY = Math.floor(y + titleH + pad); const innerW = Math.floor(w - pad * 2 - side * 2); @@ -270,7 +302,7 @@ class MemorySubsystemVisualization { 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.fillText(String(row.label || row.id || '').slice(0, 28), 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); @@ -359,7 +391,7 @@ class MemorySubsystemVisualization { const gap = 16; const top = 58; - const statsH = 98; + const statsH = 128; const graphY = top + statsH + gap; const graphH = Math.max(260, h - graphY - 36); diff --git a/static/js/right-semicircle-menu.js b/static/js/right-semicircle-menu.js index 9070a44..dc5f9f8 100644 --- a/static/js/right-semicircle-menu.js +++ b/static/js/right-semicircle-menu.js @@ -91,7 +91,7 @@ class RightSemicircleMenuManager { return; } if (itemId === 'processes') { - window.location.assign('/linux-processes-subsystem.html'); + window.location.assign('/linux-processes-subsystem'); return; } if (itemId === 'memory') { @@ -101,6 +101,7 @@ class RightSemicircleMenuManager { if (!window.kernelContextMenu) return; if (itemId === 'kernel') { window.kernelContextMenu.activateDNAView(); + return; } else if (itemId === 'network') { window.kernelContextMenu.activateNetworkView(); } else if (itemId === 'devices') { @@ -304,7 +305,6 @@ class RightSemicircleMenuManager { this.setItemHoverState(itemGroup, true, hudStrokeHair, hudStrokeNormal, hudStrokeAccent); } - // Hide submenu for dedicated-page items. if (window.kernelContextMenu) { window.kernelContextMenu.hideSubmenu(); } @@ -315,14 +315,6 @@ class RightSemicircleMenuManager { // Проверяем, не перешли ли мы на другой элемент меню или подменю const relatedTarget = event.relatedTarget; - // Если ушли на подменю Processes, не сбрасываем hover - if (item.id === 'processes' && window.kernelContextMenu) { - const submenu = d3.select('.kernel-submenu').node(); - if (submenu && (submenu.contains(relatedTarget) || submenu === relatedTarget)) { - return; // Не сбрасываем, если перешли на подменю - } - } - // Проверяем, не перешли ли на другой элемент этого же меню if (relatedTarget) { const parentGroup = relatedTarget.closest ? relatedTarget.closest('.right-menu-item-group') : null; @@ -333,20 +325,6 @@ class RightSemicircleMenuManager { // Reset hover immediately to avoid perceived "stuck active" delay. if (this.hoveredItemId === item.id) { - // Keep hover only while pointer is over Processes submenu. - if (item.id === 'processes' && window.kernelContextMenu) { - const submenu = d3.select('.kernel-submenu').node(); - if (submenu) { - const submenuRect = submenu.getBoundingClientRect(); - const mouseX = event.clientX || 0; - const mouseY = event.clientY || 0; - if (mouseX >= submenuRect.left && mouseX <= submenuRect.right && - mouseY >= submenuRect.top && mouseY <= submenuRect.bottom) { - return; - } - } - } - this.hoveredItemId = null; this.setItemHoverState(itemGroup, false, hudStrokeHair, hudStrokeNormal, hudStrokeAccent); @@ -365,7 +343,7 @@ class RightSemicircleMenuManager { return; } if (item.id === 'processes') { - window.location.assign('/linux-processes-subsystem.html'); + window.location.assign('/linux-processes-subsystem'); return; } if (item.id === 'memory') { @@ -378,11 +356,6 @@ class RightSemicircleMenuManager { } if (isOverlay) { this.activateOverlayView(item.id); - } else { - debugLog('⚠️ Click on kernel item but conditions not met:', { - itemId: item.id, - hasContextMenu: !!window.kernelContextMenu - }); } }; @@ -397,7 +370,7 @@ class RightSemicircleMenuManager { return; } if (item.id === 'processes') { - window.location.assign('/linux-processes-subsystem.html'); + window.location.assign('/linux-processes-subsystem'); return; } if (item.id === 'memory') {