diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index cde8d4ed2..97a8f3e2f 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -210,6 +210,7 @@ function buildSidebarMenu() { getOptionsHTML($forLeftBar = true) . getLogHTML($forLeftBar = true) . getDevicesHTML($forLeftBar = true) . + getIntelGpuHTML($forLeftBar = true) . getGroupsHTML($view, $forLeftBar = true) . getFilterHTML($view, $forLeftBar = true) . getSnapshotsHTML($view, $forLeftBar = true) . @@ -511,6 +512,7 @@ function getNormalNavBarHTML($running, $user, $bandwidth_options, $view, $skin) echo getOptionsHTML(); echo getLogHTML(); echo getDevicesHTML(); + echo getIntelGpuHTML(); echo getGroupsHTML($view); echo getFilterHTML($view); echo getCycleHTML($view); @@ -668,6 +670,7 @@ function getCollapsedNavBarHTML($running, $user, $bandwidth_options, $view, $ski echo getOptionsHTML(); echo getLogHTML(); echo getDevicesHTML(); + echo getIntelGpuHTML(); echo getGroupsHTML($view); echo getFilterHTML($view); echo getCycleHTML($view); @@ -1132,6 +1135,34 @@ function getDevicesHTML($forLeftBar = false) { return $result; } +// Returns the html representing the Intel GPU status menu item +function getIntelGpuHTML($forLeftBar = false) { + $result = ''; + + // Only show if intel_gpu_top is available and user can view System + if (canView('System')) { + // Check if intel_gpu_top command exists + $intel_gpu_top_exists = shell_exec('which intel_gpu_top 2>/dev/null'); + if ($intel_gpu_top_exists) { + if ($forLeftBar) { + $result .= buildMenuItem( + $viewItemName = 'intelgpu', + $id = 'getIntelGpuHTML', + $itemName = 'Intel GPU', + $href = '?view=intelgpu', + $icon = 'memory', + $classNameForTag_A = '', + $subMenu = '' + ); + } else { + $result .= ''.PHP_EOL; + } + } + } + + return $result; +} + // Returns the html representing the Groups menu item function getGroupsHTML($view, $forLeftBar = false) { $result = ''; diff --git a/web/skins/classic/views/intelgpu.php b/web/skins/classic/views/intelgpu.php new file mode 100644 index 000000000..186fe5fb1 --- /dev/null +++ b/web/skins/classic/views/intelgpu.php @@ -0,0 +1,468 @@ +&1', $output, $returnCode); + + $jsonOutput = implode("\n", $output); + + // Check for permission error + if (strpos($jsonOutput, 'Permission denied') !== false || strpos($jsonOutput, 'CAP_PERFMON') !== false) { + return ['error' => 'Permission denied. intel_gpu_top requires CAP_PERFMON capability or root access. See "man 7 capabilities" for details.']; + } + + if ($returnCode != 0 && empty($output)) { + return ['error' => 'Failed to run intel_gpu_top. Is it installed? (apt install intel-gpu-tools)']; + } + + // intel_gpu_top outputs multiple JSON objects (one per sample), we want the last complete one + // Find the last complete JSON object + $jsonObjects = []; + $depth = 0; + $start = -1; + + for ($i = 0; $i < strlen($jsonOutput); $i++) { + $char = $jsonOutput[$i]; + if ($char === '{') { + if ($depth === 0) { + $start = $i; + } + $depth++; + } else if ($char === '}') { + $depth--; + if ($depth === 0 && $start >= 0) { + $jsonObjects[] = substr($jsonOutput, $start, $i - $start + 1); + $start = -1; + } + } + } + + if (empty($jsonObjects)) { + return ['error' => 'No valid JSON output from intel_gpu_top. Raw output: ' . substr($jsonOutput, 0, 500)]; + } + + // Use the last complete JSON object + $lastJson = end($jsonObjects); + $data = json_decode($lastJson, true); + + if ($data === null) { + return ['error' => 'Failed to parse JSON from intel_gpu_top: ' . json_last_error_msg()]; + } + + return $data; +} + +// Get list of Intel GPU devices +function getIntelGpuDevices() { + $output = []; + $returnCode = 0; + + exec('intel_gpu_top -L 2>&1', $output, $returnCode); + + if ($returnCode != 0 || empty($output)) { + return []; + } + + $devices = []; + foreach ($output as $line) { + $line = trim($line); + if (!empty($line) && strpos($line, 'card') !== false) { + $devices[] = $line; + } + } + + return $devices; +} + +// Get additional GPU info from sysfs/lspci +function getIntelGpuInfo() { + $info = [ + 'model' => 'Unknown', + 'driver' => 'Unknown', + 'pci_id' => 'Unknown', + ]; + + // Try to get GPU model from lspci + $output = []; + exec('lspci -nn 2>/dev/null | grep -i "VGA\|Display\|3D" | grep -i intel', $output); + if (!empty($output)) { + $info['model'] = trim($output[0]); + if (preg_match('/\[([0-9a-f]{4}:[0-9a-f]{4})\]/i', $output[0], $matches)) { + $info['pci_id'] = $matches[1]; + } + } + + // Try to get driver version + $output = []; + exec('cat /sys/module/i915/version 2>/dev/null', $output); + if (!empty($output)) { + $info['driver'] = trim($output[0]); + } else { + // Try modinfo + $output = []; + exec('modinfo i915 2>/dev/null | grep "^version:"', $output); + if (!empty($output)) { + $info['driver'] = trim(str_replace('version:', '', $output[0])); + } + } + + return $info; +} + +// Format percentage with color coding +function formatPercentage($value, $inverse = false) { + $value = floatval($value); + if ($inverse) { + // For RC6, higher is better (GPU is idle) + if ($value >= 80) { + $class = 'text-success'; + } else if ($value >= 50) { + $class = 'text-warning'; + } else { + $class = 'text-danger'; + } + } else { + // For usage, lower is better (less loaded) + if ($value >= 80) { + $class = 'text-danger'; + } else if ($value >= 50) { + $class = 'text-warning'; + } else { + $class = 'text-success'; + } + } + return '' . number_format($value, 1) . '%'; +} + +$gpuData = parseIntelGpuTop(); +$gpuDevices = getIntelGpuDevices(); +$gpuInfo = getIntelGpuInfo(); + +xhtmlHeaders(__FILE__, 'Intel GPU Status'); +getBodyTopHTML(); +echo getNavBarHTML(); +?> +
+

memory Intel GPU Status

+ +
+ + +
+ +
+ +
+ Error: +
+ + + +
+
+ info GPU Information +
+
+
+
+ Device: +
+
+ Driver: +
+
+ PCI ID: +
+
+ +
+
+ Available Devices: +
    + +
  • + +
+
+
+ +
+
+ +
+ +
+
+
+ speed Frequency & Power +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Requested Frequency:
Actual Frequency:
GPU Power:
Package Power:
RC6 (Idle):
Interrupts:
+
+
+
+ + +
+
+
+ swap_horiz Memory Bandwidth (IMC) +
+
+ + + + + + + + + + + + + + +
Reads:
Writes:
Total:
+ +

IMC bandwidth data not available

+ +
+
+
+
+ + + +
+
+ developer_board Engine Utilization +
+
+ + + + + + + + + + + + $engineData): + if (!is_array($engineData)) continue; + $busy = floatval($engineData['busy'] ?? 0); + $sema = floatval($engineData['sema'] ?? 0); + $wait = floatval($engineData['wait'] ?? 0); + $totalBusy += $busy; + $engineCount++; + ?> + + + + + + + + + + 0): ?> + + + + + + + + +
EngineBusySemaphore WaitMemory WaitUtilization Bar
%% +
+
+ = 10): ?>% +
+ 0): ?> +
+ + 0): ?> +
+ +
+
Average +
+
+ % +
+
+
+
+
+ + + + +
+
+ apps GPU Clients () +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
PIDName
%
+
+
+ + + +
+
+ help_outline Legend +
+
+
+
+
Engines
+
    +
  • Render/3D: 3D rendering workloads
  • +
  • Blitter: 2D copy operations
  • +
  • Video: Hardware video decode (VAAPI)
  • +
  • VideoEnhance: Video post-processing
  • +
+
+
+
Utilization
+
    +
  • Busy: Engine actively processing
  • +
  • Sema: Waiting on semaphore
  • +
  • Wait: Waiting on memory
  • +
+
+
+
Power States
+
    +
  • RC6: Render C-state 6 (deep idle)
  • +
  • Higher RC6 = More power efficient
  • +
  • IMC: Integrated Memory Controller
  • +
+
+
+
+
+ + +
+
+ + + + diff --git a/web/skins/classic/views/js/intelgpu.js b/web/skins/classic/views/js/intelgpu.js new file mode 100644 index 000000000..d4e59da0c --- /dev/null +++ b/web/skins/classic/views/js/intelgpu.js @@ -0,0 +1,18 @@ +function initPage() { + // Manage the BACK button + document.getElementById("backBtn").addEventListener("click", function onBackClick(evt) { + evt.preventDefault(); + window.history.back(); + }); + + // Disable the back button if there is nothing to go back to + $j('#backBtn').prop('disabled', !document.referrer.length); + + // Manage the REFRESH Button + document.getElementById("refreshBtn").addEventListener("click", function onRefreshClick(evt) { + evt.preventDefault(); + window.location.reload(true); + }); +} + +$j(document).ready(initPage);