From 1396b0b582627589f63dd1d13d11e7a83ccf289c Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 15 Jul 2025 11:12:59 -0400 Subject: [PATCH] Add Performance tab under Process monitor --- Services/ProcessMonitorService.qml | 197 +++++++++++++- Widgets/ProcessListDropdown.qml | 6 +- Widgets/ProcessListWidget.qml | 410 +++++++++++++++++++++++++++-- 3 files changed, 579 insertions(+), 34 deletions(-) diff --git a/Services/ProcessMonitorService.qml b/Services/ProcessMonitorService.qml index 260e41ad..bff751d2 100644 --- a/Services/ProcessMonitorService.qml +++ b/Services/ProcessMonitorService.qml @@ -10,7 +10,7 @@ Singleton { // Process list properties property var processes: [] property bool isUpdating: false - property int processUpdateInterval: 1500 + property int processUpdateInterval: 3000 // Performance control - only run when process monitor is actually visible property bool monitoringEnabled: false @@ -24,6 +24,26 @@ Singleton { property real totalCpuUsage: 0.0 property bool systemInfoAvailable: false + // Performance history for charts + property var cpuHistory: [] + property var memoryHistory: [] + property var networkHistory: ({rx: [], tx: []}) + property var diskHistory: ({read: [], write: []}) + property int historySize: 60 // Keep 60 data points + + // Per-core CPU usage + property var perCoreCpuUsage: [] + + // Network stats + property real networkRxRate: 0 // bytes/sec + property real networkTxRate: 0 // bytes/sec + property var lastNetworkStats: null + + // Disk I/O stats + property real diskReadRate: 0 // bytes/sec + property real diskWriteRate: 0 // bytes/sec + property var lastDiskStats: null + // Sorting options property string sortBy: "cpu" // "cpu", "memory", "name", "pid" property bool sortDescending: true @@ -33,12 +53,39 @@ Singleton { console.log("ProcessMonitorService: Starting initialization...") updateProcessList() console.log("ProcessMonitorService: Initialization complete") + + // Test monitoring disabled - only monitor when explicitly enabled + // testTimer.start() + } + + Timer { + id: testTimer + interval: 3000 + running: false + repeat: false + onTriggered: { + console.log("ProcessMonitorService: Starting test monitoring...") + enableMonitoring(true) + // Stop after 8 seconds + stopTestTimer.start() + } + } + + Timer { + id: stopTestTimer + interval: 8000 + running: false + repeat: false + onTriggered: { + console.log("ProcessMonitorService: Stopping test monitoring...") + enableMonitoring(false) + } } // System information monitoring Process { id: systemInfoProcess - command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu ' /proc/stat"] + command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)] running: false stdout: StdioCollector { @@ -57,6 +104,36 @@ Singleton { } } + // Network monitoring process + Process { + id: networkStatsProcess + command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + parseNetworkStats(text.trim()) + } + } + } + } + + // Disk I/O monitoring process + Process { + id: diskStatsProcess + command: ["bash", "-c", "cat /proc/diskstats | grep -E ' (sd[a-z]+|nvme[0-9]+n[0-9]+|vd[a-z]+) ' | grep -v 'p[0-9]' | awk '{print $3,$6,$10}'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + parseDiskStats(text.trim()) + } + } + } + } + // Process monitoring with ps command Process { id: processListProcess @@ -123,6 +200,8 @@ Singleton { if (root.monitoringEnabled) { updateSystemInfo() updateProcessList() + updateNetworkStats() + updateDiskStats() } } } @@ -139,9 +218,29 @@ Singleton { console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled") root.monitoringEnabled = enabled if (enabled) { + // Clear history when starting + root.cpuHistory = [] + root.memoryHistory = [] + root.networkHistory = ({rx: [], tx: []}) + root.diskHistory = ({read: [], write: []}) // Immediately update when enabled updateSystemInfo() updateProcessList() + updateNetworkStats() + updateDiskStats() + // console.log("ProcessMonitorService: Initial data collection started") + } + } + + function updateNetworkStats() { + if (!networkStatsProcess.running && root.monitoringEnabled) { + networkStatsProcess.running = true + } + } + + function updateDiskStats() { + if (!diskStatsProcess.running && root.monitoringEnabled) { + diskStatsProcess.running = true } } @@ -244,6 +343,7 @@ Singleton { function parseSystemInfo(text) { const lines = text.split('\n') let section = 'memory' + const coreUsages = [] for (let i = 0; i < lines.length; i++) { const line = lines[i].trim() @@ -289,10 +389,103 @@ Singleton { const used = total - idle - iowait root.totalCpuUsage = total > 0 ? (used / total) * 100 : 0 } + } else if (line.match(/^cpu\d+/)) { + const parts = line.split(/\s+/) + if (parts.length >= 8) { + const user = parseInt(parts[1]) + const nice = parseInt(parts[2]) + const system = parseInt(parts[3]) + const idle = parseInt(parts[4]) + const iowait = parseInt(parts[5]) + const irq = parseInt(parts[6]) + const softirq = parseInt(parts[7]) + + const total = user + nice + system + idle + iowait + irq + softirq + const used = total - idle - iowait + const usage = total > 0 ? (used / total) * 100 : 0 + coreUsages.push(usage) + } } } } + // Update per-core usage + root.perCoreCpuUsage = coreUsages + + // Update history + addToHistory(root.cpuHistory, root.totalCpuUsage) + const memoryPercent = root.totalMemoryKB > 0 ? (root.usedMemoryKB / root.totalMemoryKB) * 100 : 0 + addToHistory(root.memoryHistory, memoryPercent) + + // console.log("ProcessMonitorService: Updated - CPU:", root.totalCpuUsage.toFixed(1) + "%", "Memory:", memoryPercent.toFixed(1) + "%", "History length:", root.cpuHistory.length) + root.systemInfoAvailable = true } + + function parseNetworkStats(text) { + const lines = text.split('\n') + let totalRx = 0 + let totalTx = 0 + + for (const line of lines) { + const parts = line.trim().split(/\s+/) + if (parts.length >= 3) { + const rx = parseInt(parts[1]) + const tx = parseInt(parts[2]) + if (!isNaN(rx) && !isNaN(tx)) { + totalRx += rx + totalTx += tx + } + } + } + + if (root.lastNetworkStats) { + const timeDiff = root.processUpdateInterval / 1000 + root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff) + root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff) + + // Convert to KB/s for history + addToHistory(root.networkHistory.rx, root.networkRxRate / 1024) + addToHistory(root.networkHistory.tx, root.networkTxRate / 1024) + } + + root.lastNetworkStats = { rx: totalRx, tx: totalTx } + } + + function parseDiskStats(text) { + const lines = text.split('\n') + let totalRead = 0 + let totalWrite = 0 + + for (const line of lines) { + const parts = line.trim().split(/\s+/) + if (parts.length >= 3) { + const readSectors = parseInt(parts[1]) + const writeSectors = parseInt(parts[2]) + if (!isNaN(readSectors) && !isNaN(writeSectors)) { + totalRead += readSectors * 512 // Convert sectors to bytes + totalWrite += writeSectors * 512 + } + } + } + + if (root.lastDiskStats) { + const timeDiff = root.processUpdateInterval / 1000 + root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff) + root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff) + + // Convert to MB/s for history + addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024)) + addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024)) + } + + root.lastDiskStats = { read: totalRead, write: totalWrite } + } + + function addToHistory(array, value) { + array.push(value) + if (array.length > root.historySize) { + array.shift() + } + } } \ No newline at end of file diff --git a/Widgets/ProcessListDropdown.qml b/Widgets/ProcessListDropdown.qml index 129bd92e..29bb1442 100644 --- a/Widgets/ProcessListDropdown.qml +++ b/Widgets/ProcessListDropdown.qml @@ -281,10 +281,10 @@ PanelWindow { height: 80 radius: Theme.cornerRadiusLarge color: ProcessMonitorService.totalSwapKB > 0 ? - Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.08) : + Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) border.color: ProcessMonitorService.totalSwapKB > 0 ? - Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.2) : + Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) border.width: 1 @@ -298,7 +298,7 @@ PanelWindow { text: "Swap" font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium - color: ProcessMonitorService.totalSwapKB > 0 ? Theme.tertiary : Theme.surfaceText + color: ProcessMonitorService.totalSwapKB > 0 ? Theme.warning : Theme.surfaceText opacity: 0.8 } diff --git a/Widgets/ProcessListWidget.qml b/Widgets/ProcessListWidget.qml index 759beb59..a6ced9f8 100644 --- a/Widgets/ProcessListWidget.qml +++ b/Widgets/ProcessListWidget.qml @@ -443,10 +443,10 @@ PanelWindow { height: 80 radius: Theme.cornerRadiusLarge color: ProcessMonitorService.totalSwapKB > 0 ? - Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.08) : + Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) border.color: ProcessMonitorService.totalSwapKB > 0 ? - Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.2) : + Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) border.width: 1 @@ -460,7 +460,7 @@ PanelWindow { text: "Swap" font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium - color: ProcessMonitorService.totalSwapKB > 0 ? Theme.tertiary : Theme.surfaceText + color: ProcessMonitorService.totalSwapKB > 0 ? Theme.warning : Theme.surfaceText opacity: 0.8 } @@ -749,36 +749,365 @@ PanelWindow { // Define inline components for tabs Component { id: performanceTabComponent - Rectangle { - color: "transparent" + + Column { + anchors.fill: parent + spacing: Theme.spacingM - Column { - anchors.centerIn: parent - spacing: Theme.spacingL + // CPU Section - Compact with per-core bars + Rectangle { + width: parent.width + height: 200 + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 - Text { - text: "analytics" - font.family: Theme.iconFont - font.pixelSize: 48 - color: Theme.primary - opacity: 0.6 - anchors.horizontalCenter: parent.horizontalCenter + Column { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingS + + // CPU Header with overall usage + Row { + width: parent.width + height: 32 + spacing: Theme.spacingM + + Text { + text: "CPU" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 80 + height: 24 + radius: Theme.cornerRadiusSmall + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + anchors.verticalCenter: parent.verticalCenter + + Text { + text: ProcessMonitorService.totalCpuUsage.toFixed(1) + "%" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.primary + anchors.centerIn: parent + } + } + + Item { width: parent.width - 280; height: 1 } + + Text { + text: ProcessMonitorService.cpuCount + " cores" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + anchors.verticalCenter: parent.verticalCenter + } + } + + // Per-core CPU bars - Scrollable + ScrollView { + width: parent.width + height: parent.height - 40 + clip: true + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + Column { + width: parent.width + spacing: 6 + + Repeater { + model: ProcessMonitorService.perCoreCpuUsage.length + + Row { + width: parent.width + height: 20 + spacing: Theme.spacingS + + // Core label + Text { + text: "C" + index + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + width: 24 + anchors.verticalCenter: parent.verticalCenter + } + + // Usage bar + Rectangle { + width: parent.width - 80 + height: 6 + radius: 3 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: parent.width * Math.min(1.0, ProcessMonitorService.perCoreCpuUsage[index] / 100) + height: parent.height + radius: parent.radius + color: { + const usage = ProcessMonitorService.perCoreCpuUsage[index] + if (usage > 80) return Theme.error + if (usage > 60) return Theme.warning + return Theme.primary + } + + Behavior on width { + NumberAnimation { duration: Theme.shortDuration } + } + } + } + + // Usage percentage + Text { + text: ProcessMonitorService.perCoreCpuUsage[index] ? + ProcessMonitorService.perCoreCpuUsage[index].toFixed(0) + "%" : "0%" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Medium + color: Theme.surfaceText + width: 32 + horizontalAlignment: Text.AlignRight + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + } + } + + // Memory Section - Simplified + Rectangle { + width: parent.width + height: 80 + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + + Text { + text: "Memory" + font.pixelSize: Theme.fontSizeLarge + font.weight: Font.Bold + color: Theme.surfaceText + } + + Text { + text: ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedMemoryKB) + + " / " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalMemoryKB) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + + Item { width: Theme.spacingL; height: 1 } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + width: 200 + + Rectangle { + width: parent.width + height: 16 + radius: 8 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + + Rectangle { + width: ProcessMonitorService.totalMemoryKB > 0 ? + parent.width * (ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) : 0 + height: parent.height + radius: parent.radius + color: { + const usage = ProcessMonitorService.totalMemoryKB > 0 ? + (ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) : 0 + if (usage > 0.9) return Theme.error + if (usage > 0.7) return Theme.warning + return Theme.secondary + } + + Behavior on width { + NumberAnimation { duration: Theme.mediumDuration } + } + } + } + + Text { + text: ProcessMonitorService.totalMemoryKB > 0 ? + ((ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) * 100).toFixed(1) + "% used" : + "No data" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + } + + Item { width: parent.width - 300; height: 1 } + + // Swap info - compact + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 4 + visible: ProcessMonitorService.totalSwapKB > 0 + + Text { + text: "Swap" + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.warning + } + + Text { + text: ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedSwapKB) + + " / " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalSwapKB) + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + } + } + } + } + + // Network & Disk I/O - Combined compact view + Row { + width: parent.width + height: 80 + spacing: Theme.spacingM + + // Network I/O + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 80 + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Text { + text: "Network" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: 4 + + Text { + text: "↓" + font.pixelSize: Theme.fontSizeSmall + color: Theme.info + } + + Text { + text: formatNetworkSpeed(ProcessMonitorService.networkRxRate) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + } + + Row { + spacing: 4 + + Text { + text: "↑" + font.pixelSize: Theme.fontSizeSmall + color: Theme.error + } + + Text { + text: formatNetworkSpeed(ProcessMonitorService.networkTxRate) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + } + } + } } - Text { - text: "Performance Monitoring" - font.pixelSize: Theme.fontSizeLarge + 2 - font.weight: Font.Bold - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter - } - - Text { - text: "Real-time system performance charts\nwill be displayed here" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceVariantText - horizontalAlignment: Text.AlignHCenter - anchors.horizontalCenter: parent.horizontalCenter + // Disk I/O + Rectangle { + width: (parent.width - Theme.spacingM) / 2 + height: 80 + radius: Theme.cornerRadiusLarge + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) + border.width: 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Text { + text: "Disk" + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Bold + color: Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + spacing: Theme.spacingS + anchors.horizontalCenter: parent.horizontalCenter + + Row { + spacing: 4 + + Text { + text: "R" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + } + + Text { + text: formatDiskSpeed(ProcessMonitorService.diskReadRate) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + } + + Row { + spacing: 4 + + Text { + text: "W" + font.pixelSize: Theme.fontSizeSmall + color: Theme.warning + } + + Text { + text: formatDiskSpeed(ProcessMonitorService.diskWriteRate) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Bold + color: Theme.surfaceText + } + } + } + } } } } @@ -1168,4 +1497,27 @@ PanelWindow { show() } } + + // Helper functions for formatting + function formatNetworkSpeed(bytesPerSec) { + if (bytesPerSec < 1024) { + return bytesPerSec.toFixed(0) + " B/s" + } else if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + " KB/s" + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s" + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s" + } + } + + function formatDiskSpeed(bytesPerSec) { + if (bytesPerSec < 1024 * 1024) { + return (bytesPerSec / 1024).toFixed(1) + " KB/s" + } else if (bytesPerSec < 1024 * 1024 * 1024) { + return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s" + } else { + return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s" + } + } } \ No newline at end of file