Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ <h2>Linux Kernel Ring 0 Visualization</h2>
<script src="/static/js/nginx_files.js?v=6"></script>
<script src="/static/js/right-semicircle-menu.js?v=48"></script>
<script src="/static/js/kernel-dna.js?v=31"></script>
<script src="/static/js/network-stack.js?v=6"></script>
<script src="/static/js/network-stack.js?v=19"></script>
<script src="/static/js/devices-belt.js?v=23"></script>
<script src="/static/js/crypto-belt.js?v=50"></script>
<script src="/static/js/security-belt.js?v=13"></script>
Expand All @@ -108,6 +108,7 @@ <h2>Linux Kernel Ring 0 Visualization</h2>
console.error('❌ RightSemicircleMenuManager class NOT loaded!');
}
</script>
<script src="/static/js/main.js?v=195"></script>
<script src="/static/js/main.js?v=196"></script>
<script src="/static/js/kernel-tape.js?v=3"></script>
</body>
</html>
1 change: 1 addition & 0 deletions kernel_ai/api/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
_API_ROUTES = [
("/syscalls-realtime", "syscalls_realtime", h.syscalls_realtime, None),
("/kernel-data", "kernel_data", h.kernel_data, None),
("/io-pulse", "io_pulse", h.io_pulse, None),
("/process-kernel-map", "process_kernel_map", h.process_kernel_map, None),
("/processes", "get_processes", h.get_processes, None),
("/nginx-files", "nginx_files", h.nginx_files, None),
Expand Down
2 changes: 2 additions & 0 deletions kernel_ai/http/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from kernel_ai.http.api_handlers.kernel import (
get_execution_context,
io_open_files,
io_pulse,
kernel_data,
kernel_dna,
nginx_files,
Expand Down Expand Up @@ -58,6 +59,7 @@
"get_processes_detailed",
"ingest_frontend_logs",
"io_open_files",
"io_pulse",
"isolation_context",
"kernel_data",
"kernel_dna",
Expand Down
9 changes: 9 additions & 0 deletions kernel_ai/http/api_handlers/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ def syscalls_realtime():
)


def io_pulse():
return api_json(
lambda: {
"timestamp": datetime.now().isoformat(),
**_telemetry.get_io_pulse(),
}
)


def kernel_data():
return api_json(
lambda: {
Expand Down
98 changes: 98 additions & 0 deletions kernel_ai/services/core_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
import logging
import platform
import sys
import time

import psutil

from kernel_ai.logging_helpers import log_event

logger = logging.getLogger(__name__)

# Cached counters for per-second I/O pulse deltas (vmstat + disk_io).
_IO_PULSE_PREV = {"ts": None, "vmstat": {}, "disk": None}


def get_system_info():
"""Get system information."""
Expand Down Expand Up @@ -161,6 +165,100 @@ def get_kernel_subsystem_status():
return get_mock_kernel_subsystems()


def _read_vmstat():
"""Parse /proc/vmstat into an int dict."""
out = {}
try:
with open("/proc/vmstat", "r", encoding="utf-8", errors="ignore") as f:
for line in f:
key, _, value = line.partition(" ")
value = value.strip()
if value.isdigit():
out[key] = int(value)
except (OSError, ValueError):
pass
return out


def _io_pulse_zero():
return {
"pgfault_per_sec": 0,
"pgmajfault_per_sec": 0,
"pswpin_per_sec": 0,
"pswpout_per_sec": 0,
"disk_read_mb_s": 0.0,
"disk_write_mb_s": 0.0,
"disk_read_iops": 0,
"disk_write_iops": 0,
}


def get_io_pulse():
"""Per-second deltas for memory (page faults/swaps) and block I/O.

Stateful: the first call establishes a baseline and returns zeros.
"""
try:
if platform.system() != "Linux":
return _io_pulse_zero()

now = time.time()
vmstat = _read_vmstat()
try:
disk = psutil.disk_io_counters()
except (psutil.Error, OSError):
disk = None

prev = _IO_PULSE_PREV
prev_ts = prev.get("ts")
prev_vm = prev.get("vmstat") or {}
prev_disk = prev.get("disk")

# Update cache for next call.
_IO_PULSE_PREV["ts"] = now
_IO_PULSE_PREV["vmstat"] = vmstat
_IO_PULSE_PREV["disk"] = disk

if prev_ts is None:
return _io_pulse_zero()

dt = max(0.001, now - prev_ts)

def vm_rate(key):
delta = vmstat.get(key, 0) - prev_vm.get(key, 0)
return max(0, int(delta / dt))

result = {
"pgfault_per_sec": vm_rate("pgfault"),
"pgmajfault_per_sec": vm_rate("pgmajfault"),
"pswpin_per_sec": vm_rate("pswpin"),
"pswpout_per_sec": vm_rate("pswpout"),
"disk_read_mb_s": 0.0,
"disk_write_mb_s": 0.0,
"disk_read_iops": 0,
"disk_write_iops": 0,
}

if disk is not None and prev_disk is not None:
result["disk_read_mb_s"] = round(max(0.0, (disk.read_bytes - prev_disk.read_bytes) / dt) / (1024 * 1024), 3)
result["disk_write_mb_s"] = round(max(0.0, (disk.write_bytes - prev_disk.write_bytes) / dt) / (1024 * 1024), 3)
result["disk_read_iops"] = max(0, int((disk.read_count - prev_disk.read_count) / dt))
result["disk_write_iops"] = max(0, int((disk.write_count - prev_disk.write_count) / dt))

return result
except (OSError, ValueError, psutil.Error) as exc:
log_event(
logger,
"DEBUG",
"Failed to build io pulse",
event_dataset="kernel_ai.app",
component="services.core_observability",
operation="get_io_pulse",
event_data={"error": str(exc)},
)
return _io_pulse_zero()


def get_mock_process_kernel_map():
"""Mock data for process mapping."""
return {
Expand Down
4 changes: 4 additions & 0 deletions kernel_ai/services/telemetry_orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def get_kernel_subsystem_status():
return _core_observability_service.get_kernel_subsystem_status()


def get_io_pulse():
return _core_observability_service.get_io_pulse()


def get_process_kernel_map():
return _core_observability_service.get_process_kernel_map(
openai_available=OPENAI_AVAILABLE,
Expand Down
10 changes: 5 additions & 5 deletions static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1436,7 +1436,7 @@ function drawCentralPulseGrid(centerX, centerY) {
.attr("cy", centerY)
.attr("r", radius)
.attr("fill", "none")
.attr("stroke", "rgba(38, 38, 38, 0.18)")
.attr("stroke", "rgba(38, 38, 38, 0.09)")
.attr("stroke-width", idx === 0 ? 1 : 0.7)
.attr("stroke-dasharray", idx % 2 === 0 ? "2 7" : "1 9");
});
Expand All @@ -1454,7 +1454,7 @@ function drawCentralPulseGrid(centerX, centerY) {
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.attr("stroke", "rgba(38, 38, 38, 0.08)")
.attr("stroke", "rgba(38, 38, 38, 0.045)")
.attr("stroke-width", 0.55);
}

Expand All @@ -1465,7 +1465,7 @@ function drawCentralPulseGrid(centerX, centerY) {
.attr("cx", centerX + Math.cos(angle) * radius)
.attr("cy", centerY + Math.sin(angle) * radius)
.attr("r", i % 9 === 0 ? 1.7 : 1.05)
.attr("fill", "rgba(38, 38, 38, 0.28)");
.attr("fill", "rgba(38, 38, 38, 0.14)");
}
}

Expand Down Expand Up @@ -2302,7 +2302,7 @@ function drawProcessKernelMap2(centerX, centerY) {
.attr("stroke", "url(#lineGradient)") // Use gradient for depth
.attr("stroke-width", 0.9) // Thicker core/process links for stronger center hierarchy
.attr("data-original-stroke-width", 0.9) // Store original stroke-width for restoration
.attr("data-original-opacity", 0.05 + Math.random() * 0.03) // Store original opacity
.attr("data-original-opacity", 0.03 + Math.random() * 0.022) // Store original opacity
.attr("opacity", 0) // Start invisible
.attr("fill", "none")
.attr("stroke-dasharray", function() {
Expand All @@ -2317,7 +2317,7 @@ function drawProcessKernelMap2(centerX, centerY) {
line.transition()
.duration(300 + Math.random() * 200) // Random duration 300-500ms
.delay(i * 20) // Staggered animation
.attr("opacity", 0.05 + Math.random() * 0.03)
.attr("opacity", 0.03 + Math.random() * 0.022)
.attr("stroke-dashoffset", 0);

// Determine if this is the highlighted process
Expand Down
Loading
Loading