diff --git a/modules/Cpu.qml b/modules/Cpu.qml index 815f2cd..bfc7774 100644 --- a/modules/Cpu.qml +++ b/modules/Cpu.qml @@ -8,11 +8,28 @@ M.BarSection { tooltip: "" property int usage: 0 + Behavior on usage { + NumberAnimation { + duration: 400 + easing.type: Easing.OutCubic + } + } + property real freqGhz: 0 + Behavior on freqGhz { + NumberAnimation { + duration: 400 + easing.type: Easing.OutCubic + } + } + property var _prev: null property var _corePrev: [] property var _coreUsage: [] property var _coreFreq: [] + property var _coreHistory: [] // array of arrays, last 16 samples per core + property var _coreMaxFreq: [] // max freq in GHz per core, from cpufreq + property var _coreTypes: [] // "Performance" or "Efficiency" per core FileView { id: stat @@ -57,6 +74,17 @@ M.BarSection { } root._coreUsage = newUsage; root._corePrev = newPrev; + + // Update sparkline history + const histLen = 16; + const oldH = root._coreHistory; + const newH = []; + for (let i = 0; i < newUsage.length; i++) { + const prev = i < oldH.length ? oldH[i] : []; + const next = prev.concat([newUsage[i]]); + newH.push(next.length > histLen ? next.slice(next.length - histLen) : next); + } + root._coreHistory = newH; } } @@ -73,6 +101,28 @@ M.BarSection { } } + // Read per-core max freq once at init + Process { + running: true + command: ["sh", "-c", "for f in /sys/devices/system/cpu/cpu[0-9]*/cpufreq/cpuinfo_max_freq; do [ -f \"$f\" ] && cat \"$f\" || echo 0; done 2>/dev/null"] + stdout: StdioCollector { + onStreamFinished: { + root._coreMaxFreq = text.trim().split("\n").filter(l => l).map(l => parseInt(l) / 1e6); + } + } + } + + // Read P/E-core topology once at init + Process { + running: true + command: ["sh", "-c", "for d in /sys/devices/system/cpu/cpu[0-9]*/topology/core_type; do [ -f \"$d\" ] && cat \"$d\" || echo Performance; done 2>/dev/null"] + stdout: StdioCollector { + onStreamFinished: { + root._coreTypes = text.trim().split("\n").filter(l => l).map(l => l.trim()); + } + } + } + Timer { interval: M.Modules.cpu.interval || 1000 running: true @@ -135,7 +185,7 @@ M.BarSection { anchorItem: root accentColor: root.accentColor panelNamespace: "nova-cpu" - contentWidth: 220 + contentWidth: 260 // Header Item { @@ -171,67 +221,173 @@ M.BarSection { delegate: Item { required property int index width: hoverPanel.contentWidth - height: 20 readonly property int _u: root._coreUsage[index] ?? 0 readonly property real _f: root._coreFreq[index] ?? 0 readonly property color _barColor: root._loadColor(_u) - - Text { - id: coreLabel - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: index - color: M.Theme.base04 - font.pixelSize: M.Theme.fontSize - 2 - font.family: M.Theme.fontFamily - width: 14 + readonly property bool _throttled: { + const maxF = root._coreMaxFreq[index] ?? 0; + return maxF > 0 && _f < maxF * 0.85 && _u >= 60; + } + readonly property bool _isFirstECore: { + const types = root._coreTypes; + if (!types.length || index >= types.length) + return false; + if (types[index] !== "Efficiency") + return false; + return index === 0 || types[index - 1] !== "Efficiency"; } - Item { - id: coreBar - anchors.left: coreLabel.right - anchors.leftMargin: 6 - anchors.right: freqLabel.left - anchors.rightMargin: 6 - anchors.verticalCenter: parent.verticalCenter - height: 4 + height: _isFirstECore ? 28 : 20 - Rectangle { - anchors.fill: parent - color: M.Theme.base02 - radius: 2 + // P/E-core divider + Rectangle { + visible: parent._isFirstECore + anchors.top: parent.top + anchors.topMargin: 3 + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 16 + height: 1 + color: M.Theme.base03 + } + + // Row content pinned to bottom of delegate + Item { + anchors.bottom: parent.bottom + width: parent.width + height: 20 + + Text { + id: coreLabel + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: index + color: M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 16 } - Rectangle { - width: parent.width * (parent.parent._u / 100) - height: parent.height - color: parent.parent._barColor - radius: 2 - Behavior on width { - NumberAnimation { - duration: 150 + + Item { + id: coreBar + anchors.left: coreLabel.right + anchors.leftMargin: 6 + anchors.right: sparkline.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + height: 4 + + Rectangle { + anchors.fill: parent + color: M.Theme.base02 + radius: 2 + } + + Rectangle { + width: parent.width * (parent.parent.parent._u / 100) + height: parent.height + color: parent.parent.parent._barColor + radius: 2 + Behavior on width { + NumberAnimation { + duration: 150 + } } } } + + // Sparkline + Canvas { + id: sparkline + anchors.right: freqLabel.left + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 32 + height: 10 + + property var _hist: root._coreHistory[parent.parent.index] || [] + property color _col: parent.parent._barColor + + on_HistChanged: requestPaint() + on_ColChanged: requestPaint() + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.clearRect(0, 0, width, height); + const d = _hist; + if (!d.length) + return; + const bw = width / d.length; + ctx.fillStyle = _col.toString(); + for (let i = 0; i < d.length; i++) { + const h = Math.max(1, height * d[i] / 100); + ctx.fillRect(i * bw, height - h, Math.max(1, bw - 0.5), h); + } + } + } + + Text { + id: freqLabel + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: parent.parent._f.toFixed(2) + color: parent.parent._throttled ? M.Theme.base08 : M.Theme.base04 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + width: 34 + horizontalAlignment: Text.AlignRight + } + } + } + } + + // Process list separator + Rectangle { + width: parent.width - 16 + height: 1 + anchors.horizontalCenter: parent.horizontalCenter + color: M.Theme.base03 + } + + // Top processes by CPU + Repeater { + model: M.ProcessList.byCpu + + delegate: Item { + required property var modelData + width: hoverPanel.contentWidth + height: 20 + + Text { + anchors.left: parent.left + anchors.leftMargin: 12 + anchors.verticalCenter: parent.verticalCenter + text: modelData.cmd + color: M.Theme.base05 + font.pixelSize: M.Theme.fontSize - 2 + font.family: M.Theme.fontFamily + elide: Text.ElideRight + width: parent.width - 80 } Text { - id: freqLabel anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter - text: parent._f.toFixed(2) - color: M.Theme.base04 + text: modelData.cpu.toFixed(1) + "%" + color: root._loadColor(modelData.cpu) font.pixelSize: M.Theme.fontSize - 2 font.family: M.Theme.fontFamily - width: 32 + width: 36 horizontalAlignment: Text.AlignRight } } } - // Bottom padding Item { width: 1 height: 4