Sync config from arch

- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
This commit is contained in:
asepharyana committed 2026-09-23 15:19:12 +07:00
commit 1cdb82a76f
300 files changed
+78143

No files matched your search

@@ -0,0 +1,629 @@
import QtQuick
import Quickshell.Io
import "Model.js" as Model
// Owns the Activity panel's sampling lifecycle and derived data. Keeping process I/O,
// framing, watchdogs, and power-reader escalation here leaves Panel.qml focused
// on presentation and interaction.
Item {
id: root
width: 0
height: 0
visible: false
property var settings: ({})
property bool active: false
property bool expanded: false
property bool processPowerEnabled: true
readonly property string statsHelperPath: decodeURIComponent(
String(Qt.resolvedUrl("activity-sampler")).replace("file://", ""))
readonly property string powerHelperPath: decodeURIComponent(
String(Qt.resolvedUrl("activity-power-reader")).replace("file://", ""))
readonly property var snapshot: _snapshot
readonly property var thermalSnapshot: _thermalSnapshot
readonly property var storageSnapshot: _storageSnapshot
readonly property var gpuSnapshot: _gpuSnapshot
readonly property var gpus: _gpus
readonly property var processPower: _processPower
readonly property var processes: _processes
readonly property var metrics: _metrics
readonly property bool processMetricsReady: _processMetricsReady
readonly property bool processPowerUnavailable: _processPowerState === powerStateUnavailable
readonly property string processPowerState: _processPowerState
readonly property string powerStateStopped: "stopped"
readonly property string powerStateStartingUser: "starting-user"
readonly property string powerStateReadyUser: "ready-user"
readonly property string powerStateSwitchingRoot: "switching-root"
readonly property string powerStateStartingRoot: "starting-root"
readonly property string powerStateReadyRoot: "ready-root"
readonly property string powerStateUnavailable: "unavailable"
readonly property bool samplingProfileConfigured: String(setting("samplingSpeed", "")).trim() !== ""
readonly property var samplingProfile: Model.samplingProfile(setting("samplingSpeed", "Balanced"))
readonly property int refreshInterval: intervalSetting("refreshIntervalMs", 1500, 500, 10000, samplingProfile.resources)
readonly property int processRefreshInterval: intervalSetting("processRefreshIntervalMs", 3000, 1000, 15000, samplingProfile.processes)
readonly property int thermalRefreshInterval: intervalSetting("thermalRefreshIntervalMs", 10000, 3000, 60000, samplingProfile.thermals)
readonly property int gpuRefreshInterval: intervalSetting("gpuRefreshIntervalMs", 2000, 1000, 15000, samplingProfile.gpus)
readonly property int storageRefreshInterval: intervalSetting("storageRefreshIntervalMs", 30000, 10000, 120000, samplingProfile.storage)
readonly property int historySamples: boundedSetting("historySamples", 60, 20, 240)
property var _snapshot: Model.emptySnapshot()
property var _thermalSnapshot: Model.emptyThermalSnapshot()
property var _storageSnapshot: Model.emptyStorageSnapshot()
property var _gpuSnapshot: Model.emptyGpuSnapshot()
property var _gpus: []
property var _processPower: Model.emptyPower()
property var _processes: []
property var _metrics: Model.emptyMetrics()
property bool _processMetricsReady: false
property var _previousSnapshot: Model.emptySnapshot()
property var _previousProcessSnapshot: Model.emptyProcessSnapshot()
property var _previousProcessPowerSnapshot: Model.emptyPackagePowerSnapshot()
property var _previousGpuSnapshot: Model.emptyGpuSnapshot()
property var _statsReaderBuffer: []
property bool _statsReaderReady: false
property bool _resourceSamplePending: false
property bool _processSamplePending: false
property bool _thermalSamplePending: false
property bool _gpuSamplePending: false
property bool _storageSamplePending: false
property double _resourceDue: 0
property double _processDue: 0
property double _thermalDue: 0
property double _gpuDue: 0
property double _storageDue: 0
property var _processPowerBuffer: []
property bool _processPowerSamplePending: false
property string _processPowerState: powerStateStopped
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
function boundedSetting(name, fallback, minimum, maximum) {
var value = Number(setting(name, fallback))
if (!isFinite(value)) value = fallback
return Math.max(minimum, Math.min(maximum, value))
}
function intervalSetting(name, fallback, minimum, maximum, profileValue) {
if (samplingProfileConfigured)
return Math.max(minimum, Math.min(maximum, Number(profileValue) || fallback))
return boundedSetting(name, fallback, minimum, maximum)
}
function nextDeadline(previous, interval, now) {
if (previous <= 0) return now + interval
if (previous > now) return previous
return previous + (Math.floor((now - previous) / interval) + 1) * interval
}
function scheduleNextDeadline() {
deadlineScheduler.stop()
if (!active) return
var next = Math.min(_resourceDue, _processDue, _thermalDue, _storageDue)
if (expanded && _gpuDue > 0) next = Math.min(next, _gpuDue)
deadlineScheduler.interval = Math.max(1, Math.round(next - Date.now()))
deadlineScheduler.start()
}
function resetPeriodicDeadlines() {
var now = Date.now()
_resourceDue = now + refreshInterval
_processDue = now + processRefreshInterval
_thermalDue = now + thermalRefreshInterval
_storageDue = now + storageRefreshInterval
_gpuDue = expanded ? now + gpuRefreshInterval : 0
scheduleNextDeadline()
}
function collectDueSnapshots() {
if (!active) return
var now = Date.now()
if (_resourceDue <= now + 1) {
refreshResources()
_resourceDue = nextDeadline(_resourceDue, refreshInterval, now)
}
if (_processDue <= now + 1) {
refreshProcessCycle()
_processDue = nextDeadline(_processDue, processRefreshInterval, now)
}
if (_thermalDue <= now + 1) {
refreshThermals()
_thermalDue = nextDeadline(_thermalDue, thermalRefreshInterval, now)
}
if (_storageDue <= now + 1) {
refreshStorage()
_storageDue = nextDeadline(_storageDue, storageRefreshInterval, now)
}
if (expanded && _gpuDue > 0 && _gpuDue <= now + 1) {
refreshGpus()
_gpuDue = nextDeadline(_gpuDue, gpuRefreshInterval, now)
}
scheduleNextDeadline()
}
function statsPending(kind) {
if (kind === "resources") return _resourceSamplePending
if (kind === "processes") return _processSamplePending
if (kind === "thermals") return _thermalSamplePending
if (kind === "gpus") return _gpuSamplePending
if (kind === "storage") return _storageSamplePending
return false
}
function anyStatsPending() {
return _resourceSamplePending
|| _processSamplePending
|| _thermalSamplePending
|| _gpuSamplePending
|| _storageSamplePending
}
function setStatsPending(kind, value) {
if (kind === "resources") _resourceSamplePending = value
else if (kind === "processes") _processSamplePending = value
else if (kind === "thermals") _thermalSamplePending = value
else if (kind === "gpus") _gpuSamplePending = value
else if (kind === "storage") _storageSamplePending = value
}
function resetStatsReaderState() {
_statsReaderReady = false
_statsReaderBuffer = []
_resourceSamplePending = false
_processSamplePending = false
_thermalSamplePending = false
_gpuSamplePending = false
_storageSamplePending = false
statsReaderWatchdog.stop()
}
function startStatsReader() {
if (!active || statsReaderProc.running) return
_statsReaderReady = false
_statsReaderBuffer = []
statsReaderProc.running = true
}
function sendStatsRequest(kind) {
if (!active || statsPending(kind)) return
setStatsPending(kind, true)
statsReaderWatchdog.restart()
if (!statsReaderProc.running) startStatsReader()
if (_statsReaderReady) statsReaderProc.write(kind + "\n")
}
function sendPendingStatsRequests() {
if (!_statsReaderReady) return
if (_resourceSamplePending) statsReaderProc.write("resources\n")
if (_processSamplePending) statsReaderProc.write("processes\n")
if (_thermalSamplePending) statsReaderProc.write("thermals\n")
if (_gpuSamplePending) statsReaderProc.write("gpus\n")
if (_storageSamplePending) statsReaderProc.write("storage\n")
}
function refreshResources() {
sendStatsRequest("resources")
}
function refreshProcesses() {
sendStatsRequest("processes")
}
function refreshThermals() {
sendStatsRequest("thermals")
}
function refreshGpus() {
if (expanded) sendStatsRequest("gpus")
}
function refreshStorage() {
sendStatsRequest("storage")
}
function consumeStatsReaderLine(line) {
var value = String(line || "")
if (value.indexOf("snapshot-end\t") !== 0) {
_statsReaderBuffer.push(value)
return
}
var kind = value.slice("snapshot-end\t".length)
var raw = _statsReaderBuffer.join("\n")
_statsReaderBuffer = []
setStatsPending(kind, false)
if (anyStatsPending()) statsReaderWatchdog.restart()
else statsReaderWatchdog.stop()
if (kind === "resources") consumeSnapshot(raw)
else if (kind === "processes") consumeProcesses(raw)
else if (kind === "thermals") consumeThermals(raw)
else if (kind === "gpus") consumeGpus(raw)
else if (kind === "storage") consumeStorage(raw)
}
function processPowerUsesUserReader() {
return _processPowerState === powerStateStartingUser
|| _processPowerState === powerStateReadyUser
}
function processPowerReaderReady() {
return _processPowerState === powerStateReadyUser
|| _processPowerState === powerStateReadyRoot
}
function startProcessPowerReader() {
if (!active || !expanded || !processPowerEnabled
|| processPowerUnavailable || processPowerProc.running) return
var useRoot = _processPowerState === powerStateSwitchingRoot
_processPowerBuffer = []
_processPowerState = useRoot ? powerStateStartingRoot : powerStateStartingUser
processPowerProc.command = useRoot
? [powerHelperPath]
: [statsHelperPath, "--activity-process-power-reader"]
processPowerProc.running = true
}
function requestProcessPowerSample() {
if (!active || !expanded || !processPowerEnabled
|| processPowerUnavailable || _processPowerSamplePending) return
_processPowerSamplePending = true
processPowerWatchdog.restart()
if (!processPowerProc.running) startProcessPowerReader()
if (processPowerReaderReady()) processPowerProc.write("sample\n")
}
function stopProcessPowerReader() {
_processPowerState = powerStateStopped
_processPowerSamplePending = false
_processPowerBuffer = []
processPowerWatchdog.stop()
if (processPowerProc.running) processPowerProc.running = false
}
function consumeProcessPowerLine(line) {
var value = String(line || "")
if (value !== "snapshot-end\tpower") {
_processPowerBuffer.push(value)
return
}
var raw = _processPowerBuffer.join("\n")
_processPowerBuffer = []
_processPowerSamplePending = false
processPowerWatchdog.stop()
consumeProcessPower(raw)
}
function refreshProcessCycle() {
if (expanded && processPowerEnabled && !processPowerUnavailable) requestProcessPowerSample()
else refreshProcesses()
}
function refresh() {
refreshResources()
refreshProcessCycle()
refreshThermals()
refreshGpus()
refreshStorage()
}
function consumeSnapshot(raw) {
var next = Model.parseSnapshot(raw)
if (next.schema !== 1 || next.sample <= 0) return
if (Model.sameTopology(_snapshot.topology, next.topology))
next.topology = _snapshot.topology
if (_previousSnapshot.sample <= 0) {
_metrics = Model.baselineMetrics(next, metrics)
_previousSnapshot = next
_snapshot = next
if (active) resourceWarmup.restart()
return
}
_metrics = Model.nextMetrics(_previousSnapshot, next, metrics, historySamples)
_previousSnapshot = next
_snapshot = next
}
function consumeThermals(raw) {
var next = Model.parseThermalSnapshot(raw)
if (next.schema !== 1 || next.sample <= 0) return
_thermalSnapshot = next
}
function consumeStorage(raw) {
var next = Model.parseStorageSnapshot(raw)
if (next.schema !== 1 || next.sample <= 0) return
_storageSnapshot = next
}
function consumeGpus(raw) {
if (!expanded) return
var next = Model.parseGpuSnapshot(raw)
if (next.schema !== 1 || next.sample <= 0) return
if (_previousGpuSnapshot.sample <= 0) {
if (_gpus.length === 0) _gpus = Model.nextGpus(_previousGpuSnapshot, next)
_previousGpuSnapshot = next
_gpuSnapshot = next
if (active) gpuWarmup.restart()
return
}
_gpus = Model.nextGpus(_previousGpuSnapshot, next)
_previousGpuSnapshot = next
_gpuSnapshot = next
}
function consumeProcessPower(raw) {
if (!expanded || !processPowerEnabled) return
var next = Model.parsePackagePowerSnapshot(raw)
if (next.schema !== 1 || next.sample <= 0 || !Model.packageEnergyReadable(next)) {
rejectProcessPowerSample()
return
}
var needsWarmup = _previousProcessPowerSnapshot.sample <= 0
_processPower = Model.nextPackagePower(_previousProcessPowerSnapshot, next)
_previousProcessPowerSnapshot = next
if (needsWarmup && active) powerWarmup.restart()
refreshProcesses()
}
function rejectProcessPowerSample() {
var retryAsRoot = processPowerUsesUserReader()
if (retryAsRoot) {
resetProcessPowerMeasurement()
_processPowerState = powerStateSwitchingRoot
} else {
markProcessPowerUnavailable()
refreshProcesses()
}
if (processPowerProc.running) processPowerProc.running = false
}
function consumeProcesses(raw) {
var next = Model.parseProcessSnapshot(raw)
if (next.schema !== 1 || next.sample <= 0) return
if (_previousProcessSnapshot.sample <= 0) {
_previousProcessSnapshot = next
if (active) processWarmup.restart()
return
}
var busyDelta = Model.processSystemBusyDelta(_previousProcessSnapshot, next)
var nextProcesses = Model.nextProcesses(
_previousProcessSnapshot,
next,
snapshot.memory.total
)
_processes = Model.estimateProcessPower(
nextProcesses,
processPower.available ? processPower.watts : -1,
busyDelta
)
_previousProcessSnapshot = next
_processMetricsReady = true
}
function resetProcessPowerMeasurement() {
_previousProcessPowerSnapshot = Model.emptyPackagePowerSnapshot()
_processPower = Model.emptyPower()
_processes = Model.estimateProcessPower(processes, -1, -1)
}
function resetGpuMeasurement() {
_previousGpuSnapshot = Model.emptyGpuSnapshot()
}
function markProcessPowerUnavailable() {
_processPowerState = powerStateUnavailable
resetProcessPowerMeasurement()
}
function resetSessionSampling() {
_previousSnapshot = Model.emptySnapshot()
_previousProcessSnapshot = Model.emptyProcessSnapshot()
_previousProcessPowerSnapshot = Model.emptyPackagePowerSnapshot()
_previousGpuSnapshot = Model.emptyGpuSnapshot()
resetProcessPowerMeasurement()
resetStatsReaderState()
_processPowerBuffer = []
_processPowerSamplePending = false
_processPowerState = powerStateStopped
}
function startSession() {
resetSessionSampling()
refresh()
resetPeriodicDeadlines()
}
function stopSession() {
deadlineScheduler.stop()
resetStatsReaderState()
if (statsReaderProc.running) statsReaderProc.running = false
stopProcessPowerReader()
resourceWarmup.stop()
processWarmup.stop()
gpuWarmup.stop()
powerWarmup.stop()
statsReaderRetry.stop()
}
onActiveChanged: {
if (active) startSession()
else stopSession()
}
onExpandedChanged: {
if (!expanded) {
stopProcessPowerReader()
resetProcessPowerMeasurement()
resetGpuMeasurement()
_gpuDue = 0
scheduleNextDeadline()
} else if (active) {
if (processPowerEnabled)
_previousProcessPowerSnapshot = Model.emptyPackagePowerSnapshot()
_previousGpuSnapshot = Model.emptyGpuSnapshot()
refreshProcessCycle()
refreshGpus()
_gpuDue = Date.now() + gpuRefreshInterval
scheduleNextDeadline()
}
}
onProcessPowerEnabledChanged: {
if (!processPowerEnabled) {
stopProcessPowerReader()
resetProcessPowerMeasurement()
if (active) refreshProcesses()
} else if (active && expanded) {
_previousProcessPowerSnapshot = Model.emptyPackagePowerSnapshot()
refreshProcessCycle()
}
}
onRefreshIntervalChanged: if (active) resetPeriodicDeadlines()
onProcessRefreshIntervalChanged: if (active) resetPeriodicDeadlines()
onThermalRefreshIntervalChanged: if (active) resetPeriodicDeadlines()
onGpuRefreshIntervalChanged: if (active) resetPeriodicDeadlines()
onStorageRefreshIntervalChanged: if (active) resetPeriodicDeadlines()
Process {
id: statsReaderProc
command: [root.statsHelperPath, "--activity-reader"]
stdinEnabled: true
stdout: SplitParser {
onRead: function(line) { root.consumeStatsReaderLine(line) }
}
onStarted: {
root._statsReaderReady = true
root.sendPendingStatsRequests()
}
onExited: {
root.resetStatsReaderState()
if (root.active) statsReaderRetry.restart()
}
}
Process {
id: processPowerProc
stdinEnabled: true
stdout: SplitParser {
onRead: function(line) { root.consumeProcessPowerLine(line) }
}
onStarted: {
if (!root.active || !root.expanded || !root.processPowerEnabled) {
root.stopProcessPowerReader()
return
}
if (root._processPowerState === root.powerStateStartingUser)
root._processPowerState = root.powerStateReadyUser
else if (root._processPowerState === root.powerStateStartingRoot)
root._processPowerState = root.powerStateReadyRoot
else {
root.stopProcessPowerReader()
return
}
if (root._processPowerSamplePending) processPowerProc.write("sample\n")
}
onExited: {
var exitedState = root._processPowerState
root._processPowerSamplePending = false
root._processPowerBuffer = []
processPowerWatchdog.stop()
if (!root.active || !root.expanded || !root.processPowerEnabled) {
root._processPowerState = root.powerStateStopped
return
}
if (exitedState === root.powerStateStopped) {
root._processPowerState = root.powerStateStopped
Qt.callLater(function() { root.requestProcessPowerSample() })
return
}
if (exitedState === root.powerStateUnavailable) return
if (exitedState === root.powerStateSwitchingRoot
|| exitedState === root.powerStateStartingUser
|| exitedState === root.powerStateReadyUser) {
root._processPowerState = root.powerStateSwitchingRoot
Qt.callLater(function() { root.requestProcessPowerSample() })
return
}
root.markProcessPowerUnavailable()
Qt.callLater(function() { root.refreshProcesses() })
}
}
Timer {
id: statsReaderRetry
interval: 250
repeat: false
onTriggered: if (root.active) root.refresh()
}
Timer {
id: resourceWarmup
interval: 300
repeat: false
onTriggered: if (root.active) root.refreshResources()
}
Timer {
id: processWarmup
interval: 300
repeat: false
onTriggered: if (root.active) root.refreshProcesses()
}
Timer {
id: gpuWarmup
interval: 300
repeat: false
onTriggered: if (root.active && root.expanded) root.refreshGpus()
}
Timer {
id: powerWarmup
interval: 300
repeat: false
onTriggered: if (root.active && root.expanded && root.processPowerEnabled)
root.requestProcessPowerSample()
}
Timer {
id: statsReaderWatchdog
interval: 5000
repeat: false
onTriggered: {
root._statsReaderReady = false
if (statsReaderProc.running) statsReaderProc.running = false
}
}
Timer {
id: processPowerWatchdog
interval: 5000
repeat: false
onTriggered: if (processPowerProc.running) processPowerProc.running = false
}
Timer {
id: deadlineScheduler
repeat: false
onTriggered: root.collectDueSnapshots()
}
}
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) David Heinemeier Hansson
Copyright (c) 2026 Kristoffer Haugland
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,22 @@
CXX ?= c++
CPPFLAGS ?=
CXXFLAGS ?= -O2 -pipe
LDFLAGS ?=
LDLIBS ?= -ldl
SAMPLER_FLAGS = -std=c++17 -DNDEBUG -fno-exceptions -fno-rtti \
-fstack-protector-strong -fcf-protection -D_FORTIFY_SOURCE=3 \
-Wall -Wextra -Wpedantic -Werror -ffile-prefix-map=$(CURDIR)=.
SAMPLER_LDFLAGS = -Wl,--as-needed -Wl,-z,relro -Wl,-z,now \
-Wl,--build-id=none -s
.PHONY: all clean
all: activity-sampler
activity-sampler: activity-sampler.cpp
LC_ALL=C $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(SAMPLER_FLAGS) \
$(LDFLAGS) $(SAMPLER_LDFLAGS) -o $@ $< $(LDLIBS)
clean:
rm -f -- activity-sampler
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,51 @@
# Maintainer: Kristoffer Haugland <stappmus at gmail dot com>
pkgname=omarchy-activity-monitor-power-helper
pkgver=2.1.1
pkgrel=1
pkgdesc="Optional root-only native RAPL reader for the Omarchy Activity Monitor plugin"
arch=('x86_64')
url="https://github.com/stappmus/omarchy-activity-monitor"
license=('MIT')
depends=('gcc-libs' 'glibc' 'sudo')
makedepends=('gcc' 'make')
options=('!debug')
source=(
'activity-sampler.cpp'
'Makefile'
'stappmus-activity-monitor.sudoers'
'README.md'
'LICENSE'
)
sha256sums=(
'bc8463aaac0e75dcea5c36597d5031ab28630d078a438ca87a9044a422ee026c'
'2ea69385047c3a1d1893378e2866a464cba7ae28040c0add8a3eaa80f94700a3'
'e248f015e89bc7f3df4714e7d1e0248c4ac9cc4b4642dda6aa428741b6c4f2ca'
'e02f364d97fd1c7ef95bb94d607c857ae5052961cdd0c8ac9b24ba2626ca1480'
'dd56ead2d3379b1d8298bbd1b905188b21894c46312ea4186b2afde9b03b3184'
)
build() {
make activity-sampler
}
check() {
[[ $(./activity-sampler --version) == 'activity-sampler 2.1.1' ]]
grep -Fxq \
'%wheel ALL=(root) NOPASSWD: /usr/lib/stappmus-activity-monitor/activity-sampler --activity-process-power-reader' \
stappmus-activity-monitor.sudoers
if command -v visudo >/dev/null 2>&1; then
visudo -cf stappmus-activity-monitor.sudoers >/dev/null
fi
}
package() {
install -Dm755 activity-sampler \
"$pkgdir/usr/lib/stappmus-activity-monitor/activity-sampler"
install -Dm440 stappmus-activity-monitor.sudoers \
"$pkgdir/etc/sudoers.d/stappmus-activity-monitor"
install -Dm644 README.md \
"$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 LICENSE \
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,151 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "Model.js" as Model
// Owns the guarded process-action lifecycle. The visual panel decides when to
// offer an action; this controller owns its immutable identity, helper process,
// result interpretation, and transient status.
Item {
id: root
width: 0
height: 0
visible: false
property bool active: false
readonly property string signalHelperPath: decodeURIComponent(
String(Qt.resolvedUrl("process-signal")).replace("file://", ""))
readonly property string currentUser: Quickshell.env("USER") || Quickshell.env("LOGNAME")
readonly property bool confirmationOpen: _pendingAction !== null
readonly property var pendingAction: _pendingAction
readonly property bool running: signalProc.running
readonly property string status: _status
readonly property bool failed: _failed
property var _pendingAction: null
property var _activeAction: null
property string _status: ""
property bool _failed: false
signal confirmationRequested()
signal focusRequested()
signal refreshRequested()
function blockReason(process) {
return Model.processActionBlockReason(process, currentUser)
}
function request(process) {
if (!active || !enabled || running) return false
var reason = blockReason(process)
if (reason !== "") {
_pendingAction = null
_failed = true
_status = reason
clearStatus.restart()
focusRequested()
return false
}
_failed = false
_status = ""
clearStatus.stop()
_pendingAction = {
pid: Number(process.pid),
startTicks: Number(process.startTicks),
name: String(process.name || "process"),
user: String(process.user || ""),
action: "APP_TERM"
}
confirmationRequested()
focusRequested()
return true
}
function cancel() {
_pendingAction = null
focusRequested()
}
function confirm() {
if (!_pendingAction || running) {
cancel()
return
}
_activeAction = _pendingAction
_pendingAction = null
_failed = false
_status = "Ending " + _activeAction.name + "…"
signalProc.command = [
signalHelperPath,
_activeAction.action,
String(_activeAction.pid),
String(_activeAction.startTicks)
]
signalProc.running = true
}
function finish(exitCode) {
var action = _activeAction
var error = String(signalStderr.text || "").trim().split("\n")[0]
var result = String(signalStdout.text || "").trim().split(/\s+/)
var disposition = result.length ? result[result.length - 1] : ""
if (active) {
if (exitCode === 0) {
_failed = false
if (!action) _status = "App ended"
else if (disposition === "graceful") _status = "Closed " + action.name
else if (disposition === "escalated") _status = "Force-closed " + action.name
else _status = "Ended " + action.name
} else {
_failed = true
_status = error || "Could not end the selected app"
}
}
_activeAction = null
if (!active) return
refreshRequested()
clearStatus.restart()
focusRequested()
}
function clearSessionState() {
_pendingAction = null
_status = ""
_failed = false
clearStatus.stop()
}
onActiveChanged: if (!active) clearSessionState()
onEnabledChanged: if (!enabled) _pendingAction = null
Process {
id: signalProc
stdout: StdioCollector {
id: signalStdout
waitForEnd: true
}
stderr: StdioCollector {
id: signalStderr
waitForEnd: true
}
onExited: function(exitCode) { root.finish(exitCode) }
}
Timer {
id: clearStatus
interval: 4000
repeat: false
onTriggered: {
root._status = ""
root._failed = false
}
}
}
@@ -0,0 +1,63 @@
# Activity Monitor
A calm glance at what your machine is doing.
Open it from the bar when you want the picture — CPU, memory, network, disk, and the processes that actually matter. Close it, and it goes quiet. Nothing sits in the background burning power or attention.
It is built to be easy on your eyes and easy on the computer. One small native reader runs only while the panel is open. No extra daemons. No blinking wall of numbers.
![Compact Activity panel](preview-compact.png)
Need more? Expand for GPU and storage, a live process table, and a little die that matches *your* CPU — performance cores, efficient cores, and low-power islands drawn the way the silicon is laid out.
![Expanded Activity panel](preview-expanded.png)
Hybrid Intel, AMD Zen 5c, and ARM big.LITTLE each get their own shape. Older, even chips stay on a simple grid.
![How Activity draws different CPUs](preview-cpu-layouts.png)
## Install
```bash
omarchy plugin add https://github.com/stappmus/omarchy-activity-monitor.git --enable
```
Or from Setup → Plugins. To turn it off later:
```bash
omarchy plugin disable stappmus.activity-monitor
omarchy plugin remove stappmus.activity-monitor
```
## Using it
- Click the bar icon to open. `e` expands, Esc collapses.
- `/` search processes. `j` / `k` move. Click a column header to sort.
- `s` opens settings — update speed, graph history, °C/°F, and whether to estimate process watts.
- Left-click a process to close it. A popup asks first; `x` does the same for the selected row.
Settings are saved with your bar layout and apply immediately.
## A few honest details
Used memory follows Linux available-memory accounting, with cache shown separately because the kernel can reclaim it. GPU memory is what the driver actually reports — shared for integrated, VRAM for discrete. A power-gated GPU clock shows as `IDLE`.
Estimated watts (`~W`) are a share of measured CPU-package energy, not wall power. They only run in the expanded view, and you can turn them off.
If your kernel hides RAPL energy from users, an optional helper package can read it without a password prompt. The plugin never asks for sudo while you are watching the panel.
```bash
makepkg --cleanbuild --install
sudo pacman -Rns omarchy-activity-monitor-power-helper # to remove it later
```
## Test
```bash
make -B activity-sampler
./test/all.sh
qmllint -I /usr/share/omarchy/shell ActivityController.qml Panel.qml ProcessActionController.qml Sparkline.qml
omarchy plugin validate .
```
Licensed under the MIT License. Started as an Omarchy activity panel and maintained as an independent plugin by Kristoffer Haugland.
@@ -0,0 +1,68 @@
import QtQuick
import qs.Commons
Canvas {
id: root
property var values: []
property color lineColor: Color.accent
property color fillColor: Util.alpha(lineColor, 0.12)
property real ceiling: 0
antialiasing: true
onValuesChanged: requestPaint()
onLineColorChanged: requestPaint()
onFillColorChanged: requestPaint()
onCeilingChanged: requestPaint()
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: {
var context = getContext("2d")
context.clearRect(0, 0, width, height)
var points = Array.isArray(values) ? values : []
if (points.length === 0 || width <= 0 || height <= 0) return
var maximum = Number(ceiling)
if (!isFinite(maximum) || maximum <= 0) {
maximum = 1
for (var i = 0; i < points.length; i++) {
var candidate = Number(points[i])
if (isFinite(candidate)) maximum = Math.max(maximum, candidate)
}
maximum *= 1.12
}
var step = points.length > 1 ? width / (points.length - 1) : width
var yFor = function(value) {
var normalized = Math.max(0, Math.min(1, Number(value || 0) / maximum))
return height - normalized * Math.max(1, height - Style.spacing.hairline)
}
context.beginPath()
context.moveTo(0, height)
for (var j = 0; j < points.length; j++) {
var x = points.length > 1 ? j * step : width
context.lineTo(x, yFor(points[j]))
}
context.lineTo(width, height)
context.closePath()
context.fillStyle = fillColor
context.fill()
context.beginPath()
for (var k = 0; k < points.length; k++) {
var lineX = points.length > 1 ? k * step : width
var lineY = yFor(points[k])
if (k === 0) context.moveTo(lineX, lineY)
else context.lineTo(lineX, lineY)
}
context.strokeStyle = lineColor
context.lineWidth = Math.max(1, Style.spaceReal(1.25))
context.lineJoin = "round"
context.lineCap = "round"
context.stroke()
}
}
@@ -0,0 +1,9 @@
#!/bin/bash
set -euo pipefail
readonly helper=/usr/lib/stappmus-activity-monitor/activity-sampler
[[ $# -eq 0 ]] || exit 64
[[ -x $helper ]] || exit 77
exec sudo -n /usr/lib/stappmus-activity-monitor/activity-sampler --activity-process-power-reader
Binary file not shown.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,11 @@
#!/bin/bash
# omarchy:summary=Print system activity stats for the shell
# omarchy:group=system
# omarchy:args=[--bar-widget|--activity-reader|--activity-resources|--activity-processes|--activity-thermals|--activity-gpus|--activity-process-power|--activity-process-power-reader|--activity-storage]
set -euo pipefail
readonly helper="${BASH_SOURCE[0]%/*}/activity-sampler"
[[ -x $helper ]] || exit 77
exec "$helper" "$@"
@@ -0,0 +1,80 @@
{
"schemaVersion": 1,
"id": "stappmus.activity-monitor",
"name": "Activity Monitor",
"version": "2.1.1",
"author": "Kristoffer Haugland",
"license": "MIT",
"description": "A calm, low-overhead glance at CPU, memory, GPU, storage, and processes — with a die that matches your chip",
"kinds": [
"bar-widget"
],
"entryPoints": {
"barWidget": "Panel.qml"
},
"barWidget": {
"displayName": "Activity Monitor",
"description": "A calm system glance with an expandable process view and a CPU die that matches your hardware.",
"category": "System",
"aliases": [
"activity",
"system-monitor",
"btop"
],
"allowMultiple": false,
"defaultSection": "right",
"defaults": {
"samplingSpeed": "Balanced",
"historySamples": 60,
"temperatureUnit": "Celsius",
"showFrequencies": true,
"openExpanded": false,
"processPowerEnabled": true
},
"schema": [
{
"key": "samplingSpeed",
"type": "enum",
"label": "Update speed",
"options": ["Efficient", "Balanced", "Fast"],
"defaultValue": "Balanced",
"description": "Efficient uses fewer samples; Fast makes the graphs react sooner."
},
{
"key": "historySamples",
"type": "integer",
"label": "History samples",
"min": 20,
"max": 240,
"step": 10,
"defaultValue": 60
},
{
"key": "temperatureUnit",
"type": "enum",
"label": "Temperature unit",
"options": ["Celsius", "Fahrenheit"],
"defaultValue": "Celsius"
},
{
"key": "showFrequencies",
"type": "boolean",
"label": "Show hardware speeds",
"defaultValue": true
},
{
"key": "openExpanded",
"type": "boolean",
"label": "Open detailed view",
"defaultValue": false
},
{
"key": "processPowerEnabled",
"type": "boolean",
"label": "Process power estimates",
"defaultValue": true,
"description": "Disable to skip CPU package-energy sampling in the detailed view."
}
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

@@ -0,0 +1,359 @@
#!/usr/bin/python
# omarchy:summary=Safely signal a process selected in Activity Monitor
# omarchy:group=system
# omarchy:args=<TERM|KILL|APP_TERM> <PID> <START_TICKS>
"""Signal a same-user process or app root while guarding against PID reuse."""
from __future__ import annotations
import argparse
from dataclasses import dataclass
import os
import select
import signal
import sys
from pathlib import Path
APP_TERM_GRACE_MS = 3000
PF_KTHREAD = 0x00200000
DEAD_STATES = frozenset(("Z", "X", "x"))
PROTECTED_NAMES = frozenset(
(
b"quickshell",
b"hyprland",
b"uwsm",
b"systemd",
b"systemd-executo",
b"systemd-executor",
b"(sd-pam)",
b"dbus-broker",
b"dbus-broker-lau",
b"omarchy-hyprlan",
)
)
PROC_ROOT = Path("/proc")
def fail(message: str, status: int = 1) -> None:
print(message, file=sys.stderr)
raise SystemExit(status)
class InspectionError(Exception):
"""A procfs identity record could not be read or validated."""
@dataclass(frozen=True)
class ProcessStat:
pid: int
name: bytes
state: str
parent_pid: int
flags: int
start_ticks: int
@dataclass(frozen=True)
class ProcessStatus:
pid: int
state: str
user_ids: tuple[int, int, int, int]
def read_process_stat(proc_root: Path, pid: int) -> ProcessStat:
try:
raw = (proc_root / str(pid) / "stat").read_bytes()
except FileNotFoundError:
raise InspectionError(f"Process {pid} no longer exists") from None
except OSError as error:
raise InspectionError(f"Could not inspect process {pid}: {error}") from error
command_start = raw.find(b"(")
command_end = raw.rfind(b") ")
if command_start < 1 or command_end <= command_start:
raise InspectionError(f"Process {pid} returned an invalid stat record")
try:
record_pid = int(raw[:command_start].strip())
except ValueError:
raise InspectionError(f"Process {pid} returned an invalid PID") from None
if record_pid != pid:
raise InspectionError(f"Process {pid} returned a mismatched stat record")
name = raw[command_start + 1 : command_end]
# The remaining fields begin with field 3 (state), making starttime
# (field 22) zero-based index 19. Splitting after the final ") " also
# preserves process names containing spaces or parentheses.
fields = raw[command_end + 2 :].split()
if len(fields) <= 19:
raise InspectionError(f"Process {pid} returned an incomplete stat record")
try:
state = fields[0].decode("ascii")
parent_pid = int(fields[1])
flags = int(fields[6])
start_ticks = int(fields[19])
except (UnicodeDecodeError, ValueError):
raise InspectionError(f"Process {pid} returned malformed stat fields") from None
if len(state) != 1 or not state.isalpha():
raise InspectionError(f"Process {pid} returned an invalid state")
if parent_pid < 0 or flags < 0:
raise InspectionError(f"Process {pid} returned invalid process metadata")
if start_ticks <= 0:
raise InspectionError(f"Process {pid} returned an invalid start time")
return ProcessStat(record_pid, name, state, parent_pid, flags, start_ticks)
def read_process_status(proc_root: Path, pid: int) -> ProcessStatus:
try:
lines = (proc_root / str(pid) / "status").read_text(
encoding="ascii", errors="strict"
).splitlines()
except FileNotFoundError:
raise InspectionError(f"Process {pid} no longer exists") from None
except (OSError, UnicodeError) as error:
raise InspectionError(f"Could not inspect process {pid}: {error}") from error
values: dict[str, str] = {}
for line in lines:
key, separator, value = line.partition(":")
if separator and key in ("Pid", "State", "Uid"):
values[key] = value.strip()
if set(values) != {"Pid", "State", "Uid"}:
raise InspectionError(f"Process {pid} returned an incomplete status record")
try:
status_pid = int(values["Pid"])
user_ids_raw = values["Uid"].split()
if len(user_ids_raw) != 4:
raise ValueError
user_ids = tuple(int(value) for value in user_ids_raw)
except ValueError:
raise InspectionError(f"Process {pid} returned malformed ownership data") from None
state_fields = values["State"].split()
state = state_fields[0] if state_fields else ""
if status_pid != pid:
raise InspectionError(f"Process {pid} returned a mismatched status record")
if len(state) != 1 or not state.isalpha():
raise InspectionError(f"Process {pid} returned an invalid status state")
if any(user_id < 0 for user_id in user_ids):
raise InspectionError(f"Process {pid} returned invalid ownership data")
return ProcessStatus(status_pid, state, user_ids) # type: ignore[arg-type]
def protected_processes(proc_root: Path) -> set[int]:
"""Return this helper and every currently observable ancestor."""
protected: set[int] = set()
current_pid = os.getpid()
# A process cannot have a legitimate ancestry chain this deep. The bound
# also makes malformed procfs data fail closed without looping forever.
for _ in range(256):
if current_pid <= 0 or current_pid in protected:
break
protected.add(current_pid)
try:
current_pid = read_process_stat(proc_root, current_pid).parent_pid
except InspectionError:
break
# getppid() remains useful if the parent exited between walking self/stat
# and reading its record.
parent_pid = os.getppid()
if parent_pid > 0:
protected.add(parent_pid)
return protected
def reject_unsafe_target(
process_stat: ProcessStat, process_status: ProcessStatus, expected_start_ticks: int
) -> None:
if process_stat.start_ticks != expected_start_ticks:
fail(f"Process {process_stat.pid} changed before it could be signaled")
if process_stat.flags & PF_KTHREAD:
fail("Refusing to signal a kernel thread", 77)
if process_stat.state in DEAD_STATES or process_status.state in DEAD_STATES:
fail("Refusing to signal a zombie or dead process", 77)
if process_stat.name.lower() in PROTECTED_NAMES:
fail("Refusing to signal a protected desktop-session process", 77)
current_uid = os.getuid()
if any(user_id != current_uid for user_id in process_status.user_ids):
fail("Refusing to signal a process owned by another user", 77)
def safe_app_ancestor(
process_stat: ProcessStat,
process_status: ProcessStatus,
expected_name: bytes,
) -> bool:
"""Return whether a parent is safe to adopt as this app's signal target."""
return (
process_stat.name == expected_name
and not process_stat.flags & PF_KTHREAD
and process_stat.state not in DEAD_STATES
and process_status.state not in DEAD_STATES
and all(user_id == os.getuid() for user_id in process_status.user_ids)
)
def app_signal_target(
proc_root: Path,
selected_stat: ProcessStat,
selected_fd: int,
protected: set[int],
) -> tuple[ProcessStat, int, list[int]]:
"""Resolve the highest safe, direct same-name ancestor of a selected process."""
current_stat = selected_stat
current_fd = selected_fd
opened_fds = [selected_fd]
seen = {selected_stat.pid}
# The bound fails closed on malformed procfs ancestry without allowing an
# unbounded walk. Each retained pidfd also prevents a validated identity
# from silently becoming a recycled PID while the chain is resolved.
for _ in range(256):
parent_pid = current_stat.parent_pid
if parent_pid <= 1 or parent_pid in protected or parent_pid in seen:
break
try:
parent_fd = os.pidfd_open(parent_pid)
except (ProcessLookupError, PermissionError, OSError):
break
try:
parent_stat = read_process_stat(proc_root, parent_pid)
parent_status = read_process_status(proc_root, parent_pid)
current_check = read_process_stat(proc_root, current_stat.pid)
except InspectionError:
os.close(parent_fd)
break
# Recheck the direct edge after opening the parent pidfd. This avoids
# promoting a process that ceased to be the selected app's parent
# during inspection.
if (
current_check.start_ticks != current_stat.start_ticks
or current_check.parent_pid != parent_pid
or current_check.name != current_stat.name
or not safe_app_ancestor(
parent_stat, parent_status, selected_stat.name
)
):
os.close(parent_fd)
break
opened_fds.append(parent_fd)
seen.add(parent_pid)
current_stat = parent_stat
current_fd = parent_fd
return current_stat, current_fd, opened_fds
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Signal a same-user process or matching app root if its PID and "
"start time still match."
)
)
parser.add_argument("signal_name", choices=("TERM", "KILL", "APP_TERM"))
parser.add_argument("pid", type=int)
parser.add_argument("start_ticks", type=int)
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.pid <= 1:
fail("Refusing to signal a protected system process", 77)
if args.start_ticks <= 0:
fail("Process start time must be positive", 64)
protected = protected_processes(PROC_ROOT)
if args.pid in protected:
fail("Refusing to signal Activity Monitor or its ancestor chain", 77)
try:
pid_fd = os.pidfd_open(args.pid)
except ProcessLookupError:
fail(f"Process {args.pid} no longer exists")
except OSError as error:
fail(f"Could not open process {args.pid}: {error}")
opened_fds = [pid_fd]
app_result = ""
target_stat: ProcessStat | None = None
target_fd = pid_fd
try:
try:
process_stat = read_process_stat(PROC_ROOT, args.pid)
process_status = read_process_status(PROC_ROOT, args.pid)
except InspectionError as error:
fail(str(error))
reject_unsafe_target(process_stat, process_status, args.start_ticks)
if args.signal_name == "APP_TERM":
target_stat, target_fd, opened_fds = app_signal_target(
PROC_ROOT, process_stat, pid_fd, protected
)
try:
target_check = read_process_stat(PROC_ROOT, target_stat.pid)
target_status = read_process_status(PROC_ROOT, target_stat.pid)
except InspectionError as error:
fail(str(error))
reject_unsafe_target(
target_check, target_status, target_stat.start_ticks
)
if target_check.name != process_stat.name:
fail("App process identity changed before it could be signaled")
requested_signal = signal.SIGTERM
else:
target_stat = process_stat
requested_signal = getattr(signal, f"SIG{args.signal_name}")
signal.pidfd_send_signal(target_fd, requested_signal)
if args.signal_name == "APP_TERM":
target_poll = select.poll()
target_poll.register(target_fd, select.POLLIN)
if target_poll.poll(APP_TERM_GRACE_MS):
app_result = "graceful"
else:
try:
signal.pidfd_send_signal(target_fd, signal.SIGKILL)
app_result = "escalated"
except ProcessLookupError:
# The exact pidfd target exited between the grace-period
# timeout and escalation, so no force signal was needed.
app_result = "graceful"
except ProcessLookupError:
target_pid = target_stat.pid if target_stat else args.pid
fail(f"Process {target_pid} no longer exists")
except PermissionError:
target_pid = target_stat.pid if target_stat else args.pid
fail(f"Permission denied while signaling process {target_pid}", 77)
except OSError as error:
target_pid = target_stat.pid if target_stat else args.pid
fail(f"Could not signal process {target_pid}: {error}")
finally:
for opened_fd in opened_fds:
os.close(opened_fd)
result = f"signaled\t{target_stat.pid}\t{args.signal_name}"
if app_result:
result += f"\t{app_result}"
print(result)
if __name__ == "__main__":
main()
@@ -0,0 +1,13 @@
const activity = require('../Model.js')
const sizes = {
gap: 3,
domainGap: 8,
pad: 6,
performance: 18,
efficiency: 12,
same: 12
}
const catalog = activity.previewCoreCatalog(sizes)
process.stdout.write(JSON.stringify(catalog, null, 2))
@@ -0,0 +1 @@
%wheel ALL=(root) NOPASSWD: /usr/lib/stappmus-activity-monitor/activity-sampler --activity-process-power-reader
@@ -0,0 +1,706 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const activity = requireFromRoot('Model.js')
const storage = activity.parseStorageSnapshot([
'schema\tactivity-storage\t1',
'sample\t99.75',
'storage\t/\t4096000\t3072000\t819200',
''
].join('\n'))
assertEqual(storage.schema, 1, 'activity parses the storage schema version')
assertEqual(storage.sample, 99.75, 'activity parses the storage monotonic sample')
assertEqual(storage.path, '/', 'activity identifies the root filesystem')
assertEqual(storage.total, 4096000, 'activity parses total storage bytes')
assertEqual(storage.used, 3072000, 'activity parses used storage bytes')
assertEqual(storage.available, 819200, 'activity parses user-available storage bytes')
assertEqual(storage.volumes.length, 1, 'activity keeps a single storage line as one volume')
const volumes = activity.parseStorageSnapshot([
'schema\tactivity-storage\t1',
'sample\t12',
'storage\t/data\t200\t180\t20',
'storage\t/\t100\t20\t80',
''
].join('\n'))
assertEqual(volumes.volumes.length, 2, 'activity keeps multiple storage volumes')
assertEqual(volumes.path, '/', 'activity still exposes root as the primary storage path')
assertEqual(volumes.volumes[0].path, '/', 'activity sorts the root volume first')
assertEqual(volumes.volumes[1].path, '/data', 'activity keeps additional mountpoints')
assertEqual(activity.storageVolumeLabel('/'), '/', 'activity labels the root volume as /')
assertEqual(activity.storageVolumeLabel('/mnt/data'), 'data', 'activity labels a volume by its last path component')
assertEqual(activity.tightestStorageVolume(volumes.volumes).path, '/data', 'activity prefers the fullest volume')
assertEqual(activity.selectStorageVolume(volumes.volumes, '', false).path, '/data', 'activity defaults storage to the fullest volume')
assertEqual(activity.selectStorageVolume(volumes.volumes, '/', true).path, '/', 'activity keeps a pinned storage volume')
assertEqual(activity.selectStorageVolume(volumes.volumes, '/gone', true).path, '/data', 'activity falls back when a pinned volume disappears')
assertEqual(activity.cycleItemId(volumes.volumes, '/data', 'path', 1), '/', 'activity wraps the storage pager')
assertEqual(activity.nextCycleIndex(3, 0, -1), 2, 'activity wraps a pager backward')
const invalidStorage = activity.parseStorageSnapshot(
'schema\tactivity-storage\t1\nstorage\t/\t100\t120\t110\n'
)
assertEqual(invalidStorage.used, 100, 'activity bounds used storage by filesystem size')
assertEqual(invalidStorage.available, 100, 'activity bounds available storage by filesystem size')
const firstGpuSnapshot = activity.parseGpuSnapshot([
'schema\tactivity-gpus\t1',
'sample\t10',
'gpu\t0000:00:02.0\tIntel\txe\tIntel Arc Test\t-1\t-1\t-1\tunknown\t600',
'engine\t0000:00:02.0\tclient-a\trcs\t100\t1000\t1\tcycles',
'engine\t0000:00:02.0\tclient-b\trcs\t50\t1000\t1\tcycles',
'engine\t0000:00:02.0\tclient-a\tvcs\t40\t1000\t1\tcycles',
'gpu-memory\t0000:00:02.0\tshared\t536870912',
'gpu\t0000:04:00.0\tNVIDIA\tnvidia\tNVIDIA GeForce Test\t62\t2147483648\t8589934592\tvram\t1950',
''
].join('\n'))
const secondGpuSnapshot = activity.parseGpuSnapshot([
'schema\tactivity-gpus\t1',
'sample\t12',
'gpu\t0000:00:02.0\tIntel\txe\tIntel Arc Test\t-1\t-1\t-1\tunknown\t700',
'engine\t0000:00:02.0\tclient-a\trcs\t200\t1200\t1\tcycles',
'engine\t0000:00:02.0\tclient-b\trcs\t100\t1200\t1\tcycles',
'engine\t0000:00:02.0\tclient-a\tvcs\t80\t1200\t1\tcycles',
'gpu-memory\t0000:00:02.0\tshared\t805306368',
'gpu\t0000:04:00.0\tNVIDIA\tnvidia\tNVIDIA GeForce Test\t47\t3221225472\t8589934592\tvram\t1905',
''
].join('\n'))
const sampledGpus = activity.nextGpus(firstGpuSnapshot, secondGpuSnapshot)
assertEqual(firstGpuSnapshot.schema, 1, 'activity parses the GPU schema version')
assertEqual(firstGpuSnapshot.gpus.length, 2, 'activity keeps multiple GPU adapters separate')
assertEqual(firstGpuSnapshot.gpus[0].memoryKind, 'shared', 'activity labels DRM allocation as shared GPU memory')
assertEqual(firstGpuSnapshot.gpus[0].memoryUsed, 536870912, 'activity parses resident shared GPU memory')
assertEqual(sampledGpus[0].usage, 75, 'activity sums clients within an engine and avoids adding parallel engine classes')
assertEqual(sampledGpus[0].frequencyMHz, 700, 'activity carries the current Intel GPU clock into display metrics')
assertEqual(sampledGpus[0].memoryUsed, 805306368, 'activity carries current shared allocation into display metrics')
assertEqual(sampledGpus[1].usage, 47, 'activity prefers a native GPU utilization reading')
assertEqual(sampledGpus[1].frequencyMHz, 1905, 'activity parses the NVIDIA graphics clock')
assertEqual(sampledGpus[1].memoryTotal, 8589934592, 'activity exposes dedicated VRAM capacity')
const firstTimedGpu = activity.parseGpuSnapshot(
'schema\tactivity-gpus\t1\nsample\t20\ngpu\t0000:03:00.0\tAMD\tamdgpu\tAMD Test\t-1\t-1\t-1\tunknown\t1800\n'
+ 'engine\t0000:03:00.0\t7\tgfx\t1000000000\t-1\t1\ttime\n'
)
const secondTimedGpu = activity.parseGpuSnapshot(
'schema\tactivity-gpus\t1\nsample\t22\ngpu\t0000:03:00.0\tAMD\tamdgpu\tAMD Test\t-1\t-1\t-1\tunknown\t1750\n'
+ 'engine\t0000:03:00.0\t7\tgfx\t2000000000\t-1\t1\ttime\n'
)
assertEqual(
activity.nextGpus(firstTimedGpu, secondTimedGpu)[0].usage,
50,
'activity derives utilization from standard DRM nanosecond engine counters'
)
assertEqual(
activity.nextGpus(activity.emptyGpuSnapshot(), firstGpuSnapshot)[0].usage,
-1,
'activity waits for a baseline before presenting counter-based GPU usage'
)
const thermals = activity.parseThermalSnapshot([
'schema\tactivity-thermals\t1',
'sample\t100.25',
'temperature\thwmon0/temp1\tcoretemp\tPackage id 0\t62500',
''
].join('\n'))
assertEqual(thermals.schema, 1, 'activity parses the thermal schema version')
assertEqual(thermals.sample, 100.25, 'activity parses the thermal monotonic sample')
assertEqual(thermals.temperatures[0].value, 62.5, 'activity parses the CPU temperature')
const resources = activity.parseSnapshot(
'schema\tactivity-resources\t1\nsample\t100.00\nmemory\tMemTotal\t1024\n'
)
assertEqual(
activity.cpuTemperature(thermals.temperatures).label,
'Package id 0',
'activity selects CPU temperature from its independent thermal snapshot'
)
assertEqual(
Object.prototype.hasOwnProperty.call(resources, 'temperatures'),
false,
'activity keeps thermal state out of resource snapshots'
)
assertDeepEqual(
activity.emptyPower(),
{ available: false, watts: -1 },
'activity exposes a dedicated empty power model'
)
const firstPower = activity.parsePackagePowerSnapshot([
'schema\tactivity-process-power\t1',
'sample\t200',
'package\tintel-rapl:0\tpackage-0\t2000000\t10000000',
'package\tintel-rapl:1\tpackage-1\t4000000\t20000000',
''
].join('\n'))
const secondPower = activity.parsePackagePowerSnapshot([
'schema\tactivity-process-power\t1',
'sample\t202',
'package\tintel-rapl:0\tpackage-0\t6000000\t10000000',
'package\tintel-rapl:1\tpackage-1\t10000000\t20000000',
''
].join('\n'))
const power = activity.nextPackagePower(firstPower, secondPower)
assertEqual(power.watts, 5, 'activity sums package power from raw energy deltas')
assertEqual(power.available, true, 'activity exposes complete multi-package measurements')
assertEqual(
activity.packageEnergyReadable(secondPower),
true,
'activity accepts a complete set of readable package counters'
)
const metadataOnlyPower = activity.parsePackagePowerSnapshot([
'schema\tactivity-process-power\t1',
'sample\t203',
'package\tintel-rapl:0\tpackage-0\t\t10000000',
''
].join('\n'))
const metadataOnlyDerived = activity.nextPackagePower(activity.emptyPackagePowerSnapshot(), metadataOnlyPower)
assertEqual(metadataOnlyPower.packages[0].energy, -1, 'activity marks an unreadable energy counter')
assertEqual(metadataOnlyDerived.available, false, 'activity does not invent watts without an energy counter')
assertEqual(
activity.packageEnergyReadable(metadataOnlyPower),
false,
'activity rejects package metadata without a readable energy counter'
)
const beforeWrap = activity.parsePackagePowerSnapshot(
'schema\tactivity-process-power\t1\nsample\t300\npackage\tintel-rapl:0\tpackage-0\t9000000\t10000000\n'
)
const afterWrap = activity.parsePackagePowerSnapshot(
'schema\tactivity-process-power\t1\nsample\t302\npackage\tintel-rapl:0\tpackage-0\t1000000\t10000000\n'
)
assertEqual(
activity.nextPackagePower(beforeWrap, afterWrap).watts,
1,
'activity handles a RAPL energy counter wrap'
)
const beforeReset = activity.parsePackagePowerSnapshot(
'schema\tactivity-process-power\t1\nsample\t600\npackage\tintel-rapl:0\tpackage-0\t6000000\t10000000\n'
)
const afterReset = activity.parsePackagePowerSnapshot(
'schema\tactivity-process-power\t1\nsample\t602\npackage\tintel-rapl:0\tpackage-0\t1000000\t10000000\n'
)
assertEqual(
activity.nextPackagePower(beforeReset, afterReset).watts,
-1,
'activity rejects a decreasing RAPL counter away from its wrap boundary'
)
assertEqual(
activity.packageEnergyDelta(beforeReset.packages[0], afterReset.packages[0]),
0,
'activity treats a decreasing mid-range RAPL counter as reset'
)
const beforeMultiSocketPower = activity.parsePackagePowerSnapshot([
'schema\tactivity-process-power\t1',
'sample\t200',
'package\tintel-rapl:0\tpackage-0\t2000000\t10000000',
'package\tintel-rapl:1\tpackage-1\t4000000\t20000000',
''
].join('\n'))
const partialSocketPower = activity.parsePackagePowerSnapshot([
'schema\tactivity-process-power\t1',
'sample\t202',
'package\tintel-rapl:0\tpackage-0\t6000000\t10000000',
'package\tintel-rapl:1\tpackage-1\t\t20000000',
''
].join('\n'))
assertEqual(
activity.nextPackagePower(beforeMultiSocketPower, partialSocketPower).available,
false,
'activity does not present partial multi-socket energy as whole-system power'
)
assertEqual(
activity.packageEnergyReadable(partialSocketPower),
false,
'activity retries privilege when any processor package is unreadable'
)
const changedRange = activity.parsePackagePowerSnapshot(
'schema\tactivity-process-power\t1\nsample\t304\npackage\tintel-rapl:0\tpackage-0\t2000000\t20000000\n'
)
assertEqual(
activity.nextPackagePower(afterWrap, changedRange).watts,
-1,
'activity treats a changed RAPL range as a new counter'
)
assertEqual(
activity.packageEnergyDelta(afterWrap.packages[0], changedRange.packages[0]),
0,
'activity rejects a changed RAPL counter range'
)
const beforePackageWrap = activity.parsePackagePowerSnapshot(
'schema\tactivity-process-power\t1\nsample\t500\npackage\tintel-rapl:0\tpackage-0\t9000000\t10000000\n'
)
const afterPackageWrap = activity.parsePackagePowerSnapshot(
'schema\tactivity-process-power\t1\nsample\t502\npackage\tintel-rapl:0\tpackage-0\t1000000\t10000000\n'
)
assertEqual(
activity.nextPackagePower(beforePackageWrap, afterPackageWrap).watts,
1,
'activity process power handles a package energy counter wrap'
)
const firstProcessPowerSample = activity.parseProcessSnapshot([
'schema\tactivity-processes\t1',
'sample\t400\t100\t10000',
'process\t10\t1000\tS\t100\t1000\t100\tbrowser',
''
].join('\n'))
const secondProcessPowerSample = activity.parseProcessSnapshot([
'schema\tactivity-processes\t1',
'sample\t402\t100\t10200',
'process\t10\t1000\tR\t100\t1100\t100\tbrowser',
''
].join('\n'))
const sampledPowerProcesses = activity.nextProcesses(
firstProcessPowerSample,
secondProcessPowerSample,
1000
)
assertEqual(secondProcessPowerSample.systemBusyTicks, 10200, 'activity parses aggregate system busy ticks')
assertEqual(sampledPowerProcesses[0].cpuTicksDelta, 100, 'activity retains a process CPU tick delta')
assertEqual(
activity.processSystemBusyDelta(firstProcessPowerSample, secondProcessPowerSample),
200,
'activity derives the matching aggregate system busy tick delta'
)
const processRows = [
{ pid: 10, name: 'browser', cpuTicksDelta: 100, memory: 10 },
{ pid: 20, name: 'music', cpuTicksDelta: 50, memory: 5 },
{ pid: 30, name: 'idle', cpuTicksDelta: 25, memory: 1 },
{ pid: 40, name: 'reset', cpuTicksDelta: -25, memory: 2 }
]
const estimatedPower = activity.estimateProcessPower(processRows, 10, 200)
assertEqual(estimatedPower[0].power, 5, 'activity allocates package power by system CPU ticks')
assertEqual(estimatedPower[1].power, 2.5, 'activity allocates a second proportional package power estimate')
assertEqual(estimatedPower[2].power, 1.25, 'activity allocates a third proportional package power estimate')
assertEqual(estimatedPower[3].power, 0, 'activity treats a negative CPU delta as no package share')
assertEqual(
estimatedPower.reduce((total, process) => total + process.power, 0),
8.75,
'activity leaves kernel and unobserved package power unallocated'
)
assert(
estimatedPower[0] !== processRows[0] && !Object.prototype.hasOwnProperty.call(processRows[0], 'power'),
'activity process power estimation does not mutate input rows'
)
assertDeepEqual(
activity.estimateProcessPower(processRows, -1, 200).map(process => process.power),
[-1, -1, -1, -1],
'activity marks process power unavailable without a package measurement'
)
assertDeepEqual(
activity.estimateProcessPower(processRows, 10, 0).map(process => process.power),
[-1, -1, -1, -1],
'activity marks process power unavailable across a zero busy interval'
)
assertDeepEqual(
activity.estimateProcessPower(processRows, 10, -50).map(process => process.power),
[-1, -1, -1, -1],
'activity marks process power unavailable after a system counter reset'
)
assertDeepEqual(
activity.filterAndSortProcesses(estimatedPower, '', 'power', false)
.map(process => process.pid),
[10, 20, 30, 40],
'activity sorts processes by estimated power'
)
const overAccountedPower = activity.estimateProcessPower([
{ pid: 1, cpuTicksDelta: 80 },
{ pid: 2, cpuTicksDelta: 60 }
], 10, 100)
assert(
Math.abs(overAccountedPower.reduce((total, process) => total + process.power, 0) - 10) < 1e-9,
'activity caps over-accounted process estimates at measured package power'
)
JS
fixture_root=$(mktemp -d)
trap 'rm -rf -- "$fixture_root"' EXIT
proc_path="$fixture_root/proc"
sys_path="$fixture_root/sys"
hwmon_path="$sys_path/class/hwmon/hwmon0"
powercap_path="$sys_path/class/powercap/intel-rapl"
package_path="$powercap_path/intel-rapl:0"
subzone_path="$powercap_path/intel-rapl:0:0"
gpu_devices="$fixture_root/gpu-devices"
gpu_drivers="$fixture_root/gpu-drivers"
cpu_policy0="$sys_path/devices/system/cpu/cpufreq/policy0"
cpu_policy1="$sys_path/devices/system/cpu/cpufreq/policy1"
intel_frequency="$gpu_devices/0000:00:02.0/tile0/gt0/freq0"
amd_hwmon="$gpu_devices/0000:03:00.0/hwmon/hwmon0"
mkdir -p \
"$proc_path/net" \
"$fixture_root/root filesystem" \
"$sys_path/class/block" \
"$sys_path/class/drm/card0" \
"$sys_path/class/drm/card1" \
"$sys_path/class/drm/card2" \
"$sys_path/class/net/eth0/device" \
"$cpu_policy0" \
"$cpu_policy1" \
"$gpu_devices/0000:00:02.0" \
"$gpu_devices/0000:03:00.0" \
"$gpu_devices/0000:04:00.0" \
"$gpu_drivers/xe" \
"$gpu_drivers/amdgpu" \
"$gpu_drivers/nvidia" \
"$intel_frequency" \
"$amd_hwmon" \
"$proc_path/500/fd" \
"$proc_path/500/fdinfo" \
"$proc_path/501/fd" \
"$proc_path/501/fdinfo" \
"$hwmon_path" \
"$package_path" \
"$subzone_path"
printf '321.50 100.00\n' >"$proc_path/uptime"
printf 'cpu 100 0 50 800 10 5 5 0 0 0\ncpu0 50 0 25 400 5 2 3 0 0 0\n' >"$proc_path/stat"
printf 'MemTotal: 1024 kB\nHugePages_Total: 0\nMemAvailable: 512 kB\nCached: 128 kB\nSReclaimable: 32 kB\nSwapTotal: 0 kB\nSwapFree: 0 kB\n' >"$proc_path/meminfo"
printf '0.50 0.25 0.10 1/100 123\n' >"$proc_path/loadavg"
printf 'Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\tMTU\tWindow\tIRTT\neth0\t00000000\t00000000\t0001\t0\t0\t25\t00000000\t0\t0\t0\n' \
>"$proc_path/net/route"
printf 'Inter-| Receive | Transmit\n face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\neth0: 1000 1 0 0 0 0 0 0 2000 1 0 0 0 0 0 0\n' \
>"$proc_path/net/dev"
printf 'up\n' >"$sys_path/class/net/eth0/operstate"
printf 'DRIVER=test\n' >"$sys_path/class/net/eth0/device/uevent"
printf '2400000\n' >"$cpu_policy0/scaling_cur_freq"
printf '3200000\n' >"$cpu_policy1/scaling_cur_freq"
printf 'coretemp\n' >"$hwmon_path/name"
printf '62500\n' >"$hwmon_path/temp1_input"
printf 'Package id 0\n' >"$hwmon_path/temp1_label"
ln -s "$gpu_devices/0000:00:02.0" "$sys_path/class/drm/card0/device"
ln -s "$gpu_devices/0000:03:00.0" "$sys_path/class/drm/card1/device"
ln -s "$gpu_devices/0000:04:00.0" "$sys_path/class/drm/card2/device"
ln -s "$gpu_drivers/xe" "$gpu_devices/0000:00:02.0/driver"
ln -s "$gpu_drivers/amdgpu" "$gpu_devices/0000:03:00.0/driver"
ln -s "$gpu_drivers/nvidia" "$gpu_devices/0000:04:00.0/driver"
printf '0x8086\n' >"$gpu_devices/0000:00:02.0/vendor"
printf 'Intel Arc Fixture\n' >"$gpu_devices/0000:00:02.0/product_name"
printf '600\n' >"$intel_frequency/act_freq"
printf '0x1002\n' >"$gpu_devices/0000:03:00.0/vendor"
printf 'AMD Radeon Fixture\n' >"$gpu_devices/0000:03:00.0/product_name"
printf '47\n' >"$gpu_devices/0000:03:00.0/gpu_busy_percent"
printf '268435456\n' >"$gpu_devices/0000:03:00.0/mem_info_vram_used"
printf '2147483648\n' >"$gpu_devices/0000:03:00.0/mem_info_vram_total"
printf '1800000000\n' >"$amd_hwmon/freq1_input"
printf '0x10de\n' >"$gpu_devices/0000:04:00.0/vendor"
printf 'NVIDIA Fixture\n' >"$gpu_devices/0000:04:00.0/product_name"
printf '%s\n' \
$'drm-driver:\txe' \
$'drm-client-id:\t77' \
$'drm-pdev:\t0000:00:02.0' \
$'drm-resident-gtt:\t64 MiB' \
$'drm-cycles-rcs:\t100' \
$'drm-total-cycles-rcs:\t1000' \
>"$proc_path/500/fdinfo/5"
cp "$proc_path/500/fdinfo/5" "$proc_path/500/fdinfo/6"
ln -s /dev/dri/renderD128 "$proc_path/500/fd/5"
ln -s /dev/dri/renderD128 "$proc_path/500/fd/6"
printf '%s\n' \
$'drm-driver:\txe' \
$'drm-client-id:\t78' \
$'drm-pdev:\t0000:00:02.0' \
$'drm-resident-gtt:\t32 MiB' \
$'drm-cycles-rcs:\t50' \
$'drm-total-cycles-rcs:\t1000' \
>"$proc_path/501/fdinfo/8"
ln -s /dev/dri/renderD128 "$proc_path/501/fd/8"
printf '4096\t1000\t250\t200\n' >"$fixture_root/statfs"
printf '00000000:04:00.0\tNVIDIA GeForce Fixture\t62\t2147483648\t8589934592\t1950\n' \
>"$fixture_root/nvidia.tsv"
printf 'E:MEMORY_DEVICE_0_SPEED_MTS=7200\nE:MEMORY_DEVICE_0_CONFIGURED_SPEED_GTS=6.4\n' \
>"$fixture_root/udev-data"
storage_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_ROOT_PATH="$fixture_root/root filesystem" \
OMARCHY_SYSTEM_STATS_STATFS_FIXTURE_PATH="$fixture_root/statfs" \
"$ROOT/activity-stats" --activity-storage
)
expected_storage=$'schema\tactivity-storage\t1\nsample\t321.50\nstorage\t/\t4096000\t3072000\t819200'
[[ $storage_snapshot == "$expected_storage" ]] ||
fail "activity root storage output is exact, versioned, and byte-based" "$storage_snapshot"
pass "activity root storage output is exact, versioned, and byte-based"
printf '%s\n' \
$'/data 4096 2000 100 50' \
$'/ 4096 1000 250 200' \
>"$fixture_root/statfs-multi"
multi_storage_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_ROOT_PATH="$fixture_root/root filesystem" \
OMARCHY_SYSTEM_STATS_STATFS_FIXTURE_PATH="$fixture_root/statfs-multi" \
"$ROOT/activity-stats" --activity-storage
)
expected_multi_storage=$'schema\tactivity-storage\t1\nsample\t321.50\nstorage\t/\t4096000\t3072000\t819200\nstorage\t/data\t8192000\t7782400\t204800'
[[ $multi_storage_snapshot == "$expected_multi_storage" ]] ||
fail "activity storage output lists local volumes in stable order" "$multi_storage_snapshot"
pass "activity storage output lists local volumes in stable order"
live_storage=$("$ROOT/activity-stats" --activity-storage)
grep -Eq $'^storage\t/' <<<"$live_storage" ||
fail "activity live storage does not include the root filesystem" "$live_storage"
if findmnt -n -o FSTYPE /tmp >/dev/null 2>&1 && [[ $(findmnt -n -o FSTYPE /tmp) == tmpfs ]]; then
if grep -Eq $'^storage\t/tmp\t' <<<"$live_storage"; then
fail "activity live storage includes tmpfs" "$live_storage"
fi
fi
root_source=$(findmnt -n -o SOURCE / 2>/dev/null || true)
home_source=$(findmnt -n -o SOURCE /home 2>/dev/null || true)
if [[ -n $home_source && $home_source == "$root_source" ]] &&
grep -Eq $'^storage\t/home\t' <<<"$live_storage"; then
fail "activity storage splits subvolumes of the same device" "$live_storage"
fi
pass "activity live storage stays on local filesystems"
thermal_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
"$ROOT/activity-stats" --activity-thermals
)
expected_thermals=$'schema\tactivity-thermals\t1\nsample\t321.50\ntemperature\thwmon0/temp1\tcoretemp\tPackage id 0\t62500'
[[ $thermal_snapshot == "$expected_thermals" ]] ||
fail "activity CPU thermal output is exact and versioned" "$thermal_snapshot"
pass "activity CPU thermal output is exact and versioned"
gpu_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
OMARCHY_SYSTEM_STATS_NVIDIA_FIXTURE_PATH="$fixture_root/nvidia.tsv" \
"$ROOT/activity-stats" --activity-gpus
)
grep -Fxq $'schema\tactivity-gpus\t1' <<<"$gpu_snapshot" ||
fail "activity GPU output has its own schema"
grep -Fxq $'gpu\t0000:00:02.0\tIntel\txe\tIntel Arc Fixture\t-1\t-1\t-1\tunknown\t600' \
<<<"$gpu_snapshot" ||
fail "activity GPU output reads an Intel/Xe graphics clock"
grep -Fxq $'gpu\t0000:03:00.0\tAMD\tamdgpu\tAMD Radeon Fixture\t47\t268435456\t2147483648\tvram\t1800' \
<<<"$gpu_snapshot" ||
fail "activity GPU output reads AMD busy, VRAM, and graphics clock counters"
grep -Fxq $'gpu\t0000:04:00.0\tNVIDIA\tnvidia\tNVIDIA GeForce Fixture\t62\t2147483648\t8589934592\tvram\t1950' \
<<<"$gpu_snapshot" ||
fail "activity GPU output reads NVIDIA utilization, VRAM, and graphics clock through persistent NVML"
grep -Fxq $'gpu-memory\t0000:00:02.0\tshared\t100663296' <<<"$gpu_snapshot" ||
fail "activity GPU output does not aggregate shared DRM memory"
[[ $(grep -c $'^engine\t0000:00:02.0\t77\trcs\t' <<<"$gpu_snapshot") -eq 1 ]] ||
fail "activity GPU output double-counts duplicated DRM file descriptors"
[[ $(grep -c $'^engine\t0000:00:02.0\t78\trcs\t' <<<"$gpu_snapshot") -eq 1 ]] ||
fail "activity GPU output loses a distinct DRM client"
pass "activity GPU collector supports shared Intel, AMD, NVIDIA, and multiple adapters"
read_gpu_frame() {
local output_fd="$1"
local result_name="$2"
local line
local -n result="$result_name"
result=""
while IFS= read -r -u "$output_fd" line; do
[[ $line == $'snapshot-end\tgpus' ]] && return 0
result+="$line"$'\n'
done
return 1
}
coproc GPU_READER {
exec env \
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
OMARCHY_SYSTEM_STATS_NVIDIA_FIXTURE_PATH="$fixture_root/nvidia.tsv" \
OMARCHY_SYSTEM_STATS_GPU_DISCOVERY_INTERVAL_MS=500 \
"$ROOT/activity-sampler" --activity-reader
}
gpu_reader_pid=$GPU_READER_PID
gpu_reader_input=${GPU_READER[1]}
gpu_reader_output=${GPU_READER[0]}
printf 'gpus\n' >&"$gpu_reader_input"
read_gpu_frame "$gpu_reader_output" first_gpu_frame ||
fail "activity GPU reader stopped before its first frame"
mkdir -p "$proc_path/502/fd" "$proc_path/502/fdinfo"
printf '%s\n' \
$'drm-driver:\txe' \
$'drm-client-id:\t79' \
$'drm-pdev:\t0000:00:02.0' \
$'drm-resident-gtt:\t16 MiB' \
$'drm-cycles-rcs:\t25' \
$'drm-total-cycles-rcs:\t1000' \
>"$proc_path/502/fdinfo/9"
ln -s /dev/dri/renderD128 "$proc_path/502/fd/9"
printf 'gpus\n' >&"$gpu_reader_input"
read_gpu_frame "$gpu_reader_output" cached_gpu_frame ||
fail "activity GPU reader stopped during its cached frame"
if grep -Fq $'engine\t0000:00:02.0\t79\t' <<<"$cached_gpu_frame"; then
fail "activity GPU reader rescanned every process file descriptor on every sample"
fi
sleep 0.55
printf 'gpus\n' >&"$gpu_reader_input"
read_gpu_frame "$gpu_reader_output" rediscovered_gpu_frame ||
fail "activity GPU reader stopped before its discovery refresh"
exec {gpu_reader_input}>&-
wait "$gpu_reader_pid"
grep -Fq $'engine\t0000:00:02.0\t79\trcs\t25\t1000\t1\tcycles' \
<<<"$rediscovered_gpu_frame" ||
fail "activity GPU reader did not discover a new client after its cache interval"
pass "activity GPU reader refreshes counters without rescanning process FDs every sample"
read_thermal_frame() {
local output_fd="$1"
local result_name="$2"
local line
local -n result="$result_name"
result=""
while IFS= read -r -u "$output_fd" line; do
[[ $line == $'snapshot-end\tthermals' ]] && return 0
result+="$line"$'\n'
done
return 1
}
coproc THERMAL_READER {
exec env \
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
"$ROOT/activity-stats" --activity-reader
}
thermal_reader_pid=$THERMAL_READER_PID
thermal_reader_input=${THERMAL_READER[1]}
thermal_reader_output=${THERMAL_READER[0]}
printf 'thermals\n' >&"$thermal_reader_input"
read_thermal_frame "$thermal_reader_output" first_thermal_frame ||
fail "activity thermal reader stopped before its first frame"
grep -Fq $'temperature\thwmon0/temp1\tcoretemp\tPackage id 0\t62500' \
<<<"$first_thermal_frame" ||
fail "activity thermal reader did not cache its selected CPU sensor"
rm -- "$hwmon_path/temp1_input"
fallback_hwmon_path="$sys_path/class/hwmon/hwmon1"
mkdir -p "$fallback_hwmon_path"
printf 'k10temp\n' >"$fallback_hwmon_path/name"
printf '55000\n' >"$fallback_hwmon_path/temp1_input"
printf 'Tctl\n' >"$fallback_hwmon_path/temp1_label"
printf 'thermals\n' >&"$thermal_reader_input"
read_thermal_frame "$thermal_reader_output" second_thermal_frame ||
fail "activity thermal reader stopped before its fallback frame"
exec {thermal_reader_input}>&-
wait "$thermal_reader_pid"
grep -Fq $'temperature\thwmon1/temp1\tk10temp\tTctl\t55000' \
<<<"$second_thermal_frame" ||
fail "activity thermal reader did not rescan after its cached sensor vanished"
pass "activity thermal reader caches and safely invalidates its CPU sensor"
resource_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
OMARCHY_SYSTEM_STATS_UDEV_DATA_PATH="$fixture_root/udev-data" \
"$ROOT/activity-stats" --activity-resources
)
grep -Fxq $'schema\tactivity-resources\t1' <<<"$resource_snapshot" ||
fail "activity resources output has its own schema"
grep -Fxq $'memory\t1024\t512\t0\t0\t160' <<<"$resource_snapshot" ||
fail "activity resources output includes page cache and reclaimable slabs"
grep -Fxq $'frequency\tcpu\t2800\tMHz' <<<"$resource_snapshot" ||
fail "activity resources output averages current CPU policy frequencies"
grep -Fxq $'frequency\tmemory\t6400\tMT/s' <<<"$resource_snapshot" ||
fail "activity resources output reads the configured DDR transfer rate"
grep -Fxq $'network\teth0\t1000\t2000\tup\t1\t1' <<<"$resource_snapshot" ||
fail "activity resources do not follow an up default route without a gateway"
if grep -q '^temperature' <<<"$resource_snapshot"; then
fail "activity resources output avoids detailed sensor reads"
fi
pass "activity resources output avoids detailed sensor reads"
rm -- "$proc_path/net/route"
resource_without_route=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
OMARCHY_SYSTEM_STATS_UDEV_DATA_PATH="$fixture_root/udev-data" \
"$ROOT/activity-stats" --activity-resources
)
grep -Fxq $'schema\tactivity-resources\t1' <<<"$resource_without_route" ||
fail "activity resources fail when optional route metadata is absent"
pass "activity resources tolerate missing optional procfs inputs"
process_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_PASSWD_PATH="$fixture_root/missing-passwd" \
OMARCHY_SYSTEM_STATS_CLOCK_TICKS=100 \
"$ROOT/activity-stats" --activity-processes
)
expected_process_snapshot=$'schema\tactivity-processes\t1\nsample\t321.50\t100\t160'
[[ $process_snapshot == "$expected_process_snapshot" ]] ||
fail "activity process output includes exact aggregate busy ticks" "$process_snapshot"
pass "activity process output includes exact aggregate busy ticks"
printf 'package-0\n' >"$package_path/name"
printf '9000000\n' >"$package_path/energy_uj"
printf '10000000\n' >"$package_path/max_energy_range_uj"
printf 'core\n' >"$subzone_path/name"
printf '0\n' >"$subzone_path/enabled"
printf '1000000\n' >"$subzone_path/energy_uj"
printf '10000000\n' >"$subzone_path/max_energy_range_uj"
psys_path="$powercap_path/intel-rapl:1"
non_package_path="$powercap_path/intel-rapl:2"
uncore_path="$powercap_path/intel-rapl:0:1"
dram_path="$powercap_path/intel-rapl:0:2"
mmio_package_path="$sys_path/class/powercap/intel-rapl-mmio/intel-rapl-mmio:0"
mkdir -p "$psys_path" "$non_package_path" "$uncore_path" "$dram_path" "$mmio_package_path"
printf 'psys\n' >"$psys_path/name"
printf '8000000\n' >"$psys_path/energy_uj"
printf '10000000\n' >"$psys_path/max_energy_range_uj"
printf 'dram\n' >"$non_package_path/name"
printf '7000000\n' >"$non_package_path/energy_uj"
printf '10000000\n' >"$non_package_path/max_energy_range_uj"
printf 'uncore\n' >"$uncore_path/name"
printf '6000000\n' >"$uncore_path/energy_uj"
printf '10000000\n' >"$uncore_path/max_energy_range_uj"
printf 'dram\n' >"$dram_path/name"
printf '5000000\n' >"$dram_path/energy_uj"
printf '10000000\n' >"$dram_path/max_energy_range_uj"
printf 'package-0\n' >"$mmio_package_path/name"
printf '8000000\n' >"$mmio_package_path/energy_uj"
printf '10000000\n' >"$mmio_package_path/max_energy_range_uj"
process_power_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
"$ROOT/activity-stats" --activity-process-power
)
expected_process_power=$'schema\tactivity-process-power\t1\nsample\t321.50\npackage\tintel-rapl:0\tpackage-0\t9000000\t10000000'
[[ $process_power_snapshot == "$expected_process_power" ]] ||
fail "activity process power output is exact and includes only the RAPL package domain" "$process_power_snapshot"
pass "activity process power output is exact and includes only the RAPL package domain"
process_power_reader_snapshot=$(
printf 'ignored\nsample\nsample\n' |
OMARCHY_SYSTEM_STATS_PROC_PATH="$proc_path" \
OMARCHY_SYSTEM_STATS_SYS_PATH="$sys_path" \
"$ROOT/activity-stats" --activity-process-power-reader
)
[[ $(grep -c '^snapshot-end' <<<"$process_power_reader_snapshot") -eq 2 ]] ||
fail "activity process power reader does not frame each requested sample"
[[ $(grep -c '^package' <<<"$process_power_reader_snapshot") -eq 2 ]] ||
fail "activity process power reader does not reuse its process for requested samples"
pass "activity process power reader serves framed samples until its input closes"
@@ -0,0 +1,66 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command make
require_command g++
require_command file
require_command readelf
make -C "$ROOT" -B activity-sampler >/dev/null
file "$ROOT/activity-sampler" | grep -Fq 'ELF 64-bit' ||
fail "activity sampler is not a native 64-bit executable"
[[ $(stat -c %s "$ROOT/activity-sampler") -lt 524288 ]] ||
fail "activity sampler binary exceeds its 512 KiB size budget"
[[ $("$ROOT/activity-sampler" --version) == 'activity-sampler 2.1.1' ]] ||
fail "activity sampler binary does not match its source release"
readelf -lW "$ROOT/activity-sampler" | grep -Fq 'GNU_RELRO' ||
fail "activity sampler is missing RELRO hardening"
readelf -dW "$ROOT/activity-sampler" | grep -Fq 'BIND_NOW' ||
fail "activity sampler is missing immediate binding hardening"
pass "activity sampler builds as a compact hardened native executable"
read_native_frame() {
local output_fd="$1"
local kind="$2"
local line
while IFS= read -r -u "$output_fd" line; do
[[ $line == "snapshot-end"$'\t'"$kind" ]] && return 0
done
return 1
}
coproc NATIVE_READER { exec "$ROOT/activity-sampler" --activity-reader; }
native_reader_pid=$NATIVE_READER_PID
native_reader_input=${NATIVE_READER[1]}
native_reader_output=${NATIVE_READER[0]}
printf 'resources\n' >&"$native_reader_input"
read_native_frame "$native_reader_output" resources ||
fail "native activity reader stopped during its resource warmup"
printf 'processes\n' >&"$native_reader_input"
read_native_frame "$native_reader_output" processes ||
fail "native activity reader stopped during its process warmup"
children=$(<"/proc/$native_reader_pid/task/$native_reader_pid/children")
[[ -z $children ]] || fail "native activity reader launched a subprocess"
pss_kib=$(awk '/^Pss:/ { total += $2 } END { print total + 0 }' "/proc/$native_reader_pid/smaps")
(( pss_kib < 4096 )) ||
fail "native activity reader exceeds its 4 MiB proportional-memory budget" "PSS: ${pss_kib} KiB"
exec {native_reader_input}>&-
wait "$native_reader_pid"
pass "activity reader stays below 4 MiB PSS and launches no child collectors"
grep -Fq 'dlopen("libnvidia-ml.so.1"' "$ROOT/activity-sampler.cpp" &&
grep -Fq 'next_fd_discovery_' "$ROOT/activity-sampler.cpp" &&
grep -Fq 'std::unordered_map<pid_t, Metadata> cache_' "$ROOT/activity-sampler.cpp" ||
fail "activity sampler does not retain its expensive discovery state"
if grep -Eq 'nvidia-smi|popen[[:space:]]*\(|system[[:space:]]*\(|fork[[:space:]]*\(' \
"$ROOT/activity-sampler.cpp"; then
fail "activity sampler invokes an external collector"
fi
pass "activity sampler caches GPU/process discovery and keeps NVML in-process"
@@ -0,0 +1,33 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
sudoers_file="$ROOT/stappmus-activity-monitor.sudoers"
launcher="$ROOT/activity-power-reader"
expected='%wheel ALL=(root) NOPASSWD: /usr/lib/stappmus-activity-monitor/activity-sampler --activity-process-power-reader'
[[ -f $sudoers_file ]] ||
fail "activity process power installs its narrow sudoers rule"
[[ $(<"$sudoers_file") == "$expected" ]] ||
fail "activity process power sudoers rule permits only the exact collector command"
[[ $(wc -l <"$sudoers_file") -eq 1 ]] ||
fail "activity process power sudoers rule contains only one allowance"
if command -v visudo >/dev/null 2>&1; then
visudo -cf "$sudoers_file" >/dev/null ||
fail "activity process power sudoers rule is not valid sudoers syntax"
fi
pass "activity process power sudoers rule permits only the exact collector command"
[[ -x $launcher ]] ||
fail "activity process power launcher is not executable"
grep -Fxq 'exec sudo -n /usr/lib/stappmus-activity-monitor/activity-sampler --activity-process-power-reader' "$launcher" ||
fail "activity process power launcher does not invoke the exact guarded command"
if grep -Eq '\$@|\$\*' "$launcher"; then
fail "activity process power launcher forwards arbitrary command arguments"
fi
if "$launcher" unexpected >/dev/null 2>&1; then
fail "activity process power launcher accepts command arguments"
fi
pass "activity process power launcher preserves the exact sudoers command"
@@ -0,0 +1,513 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
victim_pid=""
cleanup() {
[[ -n $victim_pid ]] || return 0
kill -KILL "$victim_pid" 2>/dev/null || true
wait "$victim_pid" 2>/dev/null || true
}
trap cleanup EXIT
start_ticks_for() {
local pid="$1"
local stat_line stat_tail
local -a stat_fields
stat_line=$(<"/proc/$pid/stat")
stat_tail="${stat_line##*) }"
read -r -a stat_fields <<<"$stat_tail"
printf '%s\n' "${stat_fields[19]}"
}
assert_alive() {
local pid="$1"
local description="$2"
kill -0 "$pid" 2>/dev/null || fail "$description"
}
sleep 30 &
victim_pid=$!
victim_start=$(start_ticks_for "$victim_pid")
if "$ROOT/process-signal" TERM "$victim_pid" invalid 2>/dev/null; then
fail "process action rejects malformed start ticks"
fi
assert_alive "$victim_pid" "process action rejects malformed start ticks"
pass "process action rejects malformed start ticks"
if "$ROOT/process-signal" TERM "$victim_pid" 0 2>/dev/null; then
fail "process action rejects zero start ticks"
fi
assert_alive "$victim_pid" "process action rejects zero start ticks"
pass "process action rejects zero start ticks"
if "$ROOT/process-signal" TERM "$victim_pid" "$((victim_start + 1))" 2>/dev/null; then
fail "process action rejects a stale PID identity"
fi
assert_alive "$victim_pid" "process action left a process alive after rejecting stale identity"
pass "process action rejects a stale PID identity"
if "$ROOT/process-signal" APP_TERM "$victim_pid" "$((victim_start + 1))" 2>/dev/null; then
fail "app action rejects a stale selected-process identity"
fi
assert_alive "$victim_pid" "app action left a process alive after rejecting stale identity"
pass "app action rejects a stale selected-process identity"
"$ROOT/process-signal" TERM "$victim_pid" "$victim_start" >/dev/null ||
fail "process action gracefully terminates a matching process"
wait "$victim_pid" 2>/dev/null || true
if kill -0 "$victim_pid" 2>/dev/null; then
fail "process action gracefully terminates a matching process"
fi
victim_pid=""
pass "process action gracefully terminates a matching process"
python -c '
import time
with open("/proc/self/comm", "w", encoding="ascii") as comm:
comm.write("quickshell")
time.sleep(30)
' &
victim_pid=$!
for _ in {1..100}; do
[[ $(<"/proc/$victim_pid/comm") == "quickshell" ]] && break
sleep 0.01
done
[[ $(<"/proc/$victim_pid/comm") == "quickshell" ]] ||
fail "protected desktop-session fixture did not become ready"
victim_start=$(start_ticks_for "$victim_pid")
protected_result=$(
"$ROOT/process-signal" TERM "$victim_pid" "$victim_start" 2>&1
) && fail "process action signaled a protected desktop-session process"
[[ $protected_result == *"protected desktop-session process"* ]] ||
fail "process action does not explain protected desktop-session rejection" "$protected_result"
assert_alive "$victim_pid" "process action killed a protected desktop-session process"
kill -KILL "$victim_pid"
wait "$victim_pid" 2>/dev/null || true
victim_pid=""
pass "process action protects desktop-session processes in the signal helper"
python - "$ROOT/process-signal" <<'PY' ||
import os
import select
import signal
import subprocess
import sys
import textwrap
import time
helper = sys.argv[1]
app_name = "activity-app"
def start_ticks(pid):
raw = open(f"/proc/{pid}/stat", "rb").read()
return raw[raw.rfind(b") ") + 2:].split()[19].decode()
worker_code = textwrap.dedent(
f"""
import time
with open("/proc/self/comm", "w", encoding="ascii") as comm:
comm.write({app_name!r})
time.sleep(30)
"""
)
app_code = textwrap.dedent(
f"""
import signal
import subprocess
import sys
import time
with open("/proc/self/comm", "w", encoding="ascii") as comm:
comm.write({app_name!r})
worker = None
def stop_app(_signal, _frame):
if worker is not None and worker.poll() is None:
worker.terminate()
# Do not return to the supervisor loop where the worker would be
# replaced. Immediate exit also keeps this fixture deterministic when
# SIGTERM interrupts subprocess.wait().
sys.stdout.flush()
sys.stderr.flush()
raise SystemExit(0)
signal.signal(signal.SIGTERM, stop_app)
while True:
worker = subprocess.Popen(
[sys.executable, "-c", {worker_code!r}],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
for _ in range(300):
try:
worker_name = open(
f"/proc/{{worker.pid}}/comm", encoding="ascii"
).read().strip()
except FileNotFoundError:
worker_name = ""
if worker_name == {app_name!r}:
break
time.sleep(0.01)
else:
raise SystemExit(1)
print(worker.pid, flush=True)
worker.wait()
time.sleep(0.05)
"""
)
unrelated = subprocess.Popen(
[sys.executable, "-c", worker_code],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
app = subprocess.Popen(
[sys.executable, "-c", app_code],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
selected_pid = None
selected_start = None
try:
ready, _, _ = select.select([app.stdout], [], [], 3)
if not ready:
raise SystemExit(1)
selected_pid = int(app.stdout.readline().strip())
selected_start = start_ticks(selected_pid)
result = subprocess.run(
[helper, "APP_TERM", str(selected_pid), selected_start],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if result.returncode != 0:
raise SystemExit(1)
fields = result.stdout.strip().split("\t")
if fields != ["signaled", str(app.pid), "APP_TERM", "graceful"]:
raise SystemExit(1)
if app.wait(timeout=3) != 0:
raise SystemExit(1)
if app.stdout.read().strip():
# Signaling only the selected child would make the app supervisor
# replace it and announce another PID here.
raise SystemExit(1)
if unrelated.poll() is not None:
# A same-name process outside the selected direct ancestry is not part
# of the app action.
raise SystemExit(1)
stubborn_worker_code = textwrap.dedent(
f"""
import ctypes
import os
import signal
import time
with open("/proc/self/comm", "w", encoding="ascii") as comm:
comm.write({app_name!r})
libc = ctypes.CDLL(None, use_errno=True)
if libc.prctl(1, signal.SIGKILL, 0, 0, 0) != 0:
raise OSError(ctypes.get_errno(), "prctl")
if os.getppid() == 1:
raise SystemExit(1)
time.sleep(30)
"""
)
stubborn_app_code = textwrap.dedent(
f"""
import signal
import subprocess
import sys
import time
with open("/proc/self/comm", "w", encoding="ascii") as comm:
comm.write({app_name!r})
signal.signal(signal.SIGTERM, signal.SIG_IGN)
worker = subprocess.Popen(
[sys.executable, "-c", {stubborn_worker_code!r}],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
for _ in range(300):
try:
worker_name = open(
f"/proc/{{worker.pid}}/comm", encoding="ascii"
).read().strip()
except FileNotFoundError:
worker_name = ""
if worker_name == {app_name!r}:
break
time.sleep(0.01)
else:
raise SystemExit(1)
print(worker.pid, flush=True)
while True:
worker.wait()
worker = subprocess.Popen(
[sys.executable, "-c", {stubborn_worker_code!r}],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
print(worker.pid, flush=True)
"""
)
app = subprocess.Popen(
[sys.executable, "-c", stubborn_app_code],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
ready, _, _ = select.select([app.stdout], [], [], 3)
if not ready:
raise SystemExit(1)
selected_pid = int(app.stdout.readline().strip())
selected_start = start_ticks(selected_pid)
escalation_started = time.monotonic()
result = subprocess.run(
[helper, "APP_TERM", str(selected_pid), selected_start],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if result.returncode != 0:
raise SystemExit(1)
fields = result.stdout.strip().split("\t")
if fields != ["signaled", str(app.pid), "APP_TERM", "escalated"]:
raise SystemExit(1)
if time.monotonic() - escalation_started < 2.8:
raise SystemExit(1)
if app.wait(timeout=5) != -signal.SIGKILL:
raise SystemExit(1)
if unrelated.poll() is not None:
raise SystemExit(1)
finally:
if app.poll() is None:
app.terminate()
try:
app.wait(timeout=3)
except subprocess.TimeoutExpired:
app.kill()
app.wait()
if selected_pid is not None:
try:
if start_ticks(selected_pid) == selected_start:
os.kill(selected_pid, signal.SIGKILL)
except (FileNotFoundError, ProcessLookupError):
pass
if unrelated.poll() is None:
unrelated.kill()
unrelated.wait()
PY
fail "app action gracefully closes or escalates app roots without crossing boundaries"
pass "app action gracefully closes or escalates app roots without crossing boundaries"
python - "$ROOT/process-signal" <<'PY' ||
import signal
import subprocess
import sys
helper = sys.argv[1]
victim = subprocess.Popen(["sleep", "30"])
try:
raw = open(f"/proc/{victim.pid}/stat", "rb").read()
start_ticks = raw[raw.rfind(b") ") + 2:].split()[19].decode()
result = subprocess.run(
[helper, "KILL", str(victim.pid), start_ticks],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if result.returncode != 0:
raise SystemExit(1)
if victim.wait(timeout=3) != -signal.SIGKILL:
raise SystemExit(1)
finally:
if victim.poll() is None:
victim.kill()
victim.wait()
PY
fail "process action force kills a matching disposable process"
pass "process action force kills a matching disposable process"
python - "$ROOT/process-signal" <<'PY' ||
import os
import subprocess
import sys
helper = sys.argv[1]
def start_ticks(pid):
raw = open(f"/proc/{pid}/stat", "rb").read()
return raw[raw.rfind(b") ") + 2:].split()[19].decode()
ancestor_pid = os.getpid()
ancestor_start = start_ticks(ancestor_pid)
# Direct parent: the helper is subprocess.run()'s child.
result = subprocess.run(
[helper, "TERM", str(ancestor_pid), ancestor_start],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if result.returncode == 0 or "ancestor chain" not in result.stderr:
raise SystemExit(1)
# Grandparent: the forked intermediary starts the helper while its parent
# remains the protected target.
intermediary_pid = os.fork()
if intermediary_pid == 0:
nested = subprocess.run(
[helper, "TERM", str(ancestor_pid), ancestor_start],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
check=False,
)
os._exit(0 if nested.returncode != 0 and "ancestor chain" in nested.stderr else 1)
_, status = os.waitpid(intermediary_pid, 0)
if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
raise SystemExit(1)
PY
fail "process action protects its helper ancestor chain"
pass "process action protects its helper ancestor chain"
python - "$ROOT/process-signal" <<'PY' ||
import os
import subprocess
import sys
import time
helper = sys.argv[1]
zombie_pid = os.fork()
if zombie_pid == 0:
os._exit(0)
try:
for _ in range(100):
status = open(f"/proc/{zombie_pid}/status", encoding="ascii").read()
if "\nState:\tZ " in "\n" + status:
break
time.sleep(0.01)
else:
raise SystemExit(1)
raw = open(f"/proc/{zombie_pid}/stat", "rb").read()
start_ticks = raw[raw.rfind(b") ") + 2:].split()[19].decode()
result = subprocess.run(
[helper, "TERM", str(zombie_pid), start_ticks],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if result.returncode == 0 or "zombie or dead" not in result.stderr:
raise SystemExit(1)
finally:
os.waitpid(zombie_pid, 0)
PY
fail "process action rejects zombie processes"
pass "process action rejects zombie processes"
foreign_uid_test=$(python - "$ROOT/process-signal" <<'PY'
import os
from pathlib import Path
import subprocess
import sys
helper = sys.argv[1]
current_uid = os.getuid()
target = None
for status_path in Path("/proc").glob("[0-9]*/status"):
try:
status_lines = status_path.read_text(encoding="ascii").splitlines()
status = {
line.partition(":")[0]: line.partition(":")[2].strip()
for line in status_lines
if ":" in line
}
pid = int(status["Pid"])
user_ids = tuple(int(value) for value in status["Uid"].split())
raw = (status_path.parent / "stat").read_bytes()
fields = raw[raw.rfind(b") ") + 2:].split()
flags = int(fields[6])
start_ticks = int(fields[19])
except (FileNotFoundError, KeyError, OSError, ValueError, IndexError):
continue
if pid > 1 and current_uid not in user_ids and not flags & 0x00200000:
target = (pid, start_ticks)
break
if target is None:
print("skip")
raise SystemExit(0)
result = subprocess.run(
[helper, "TERM", str(target[0]), str(target[1])],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if result.returncode == 0 or "another user" not in result.stderr:
raise SystemExit(1)
print("checked")
PY
) || fail "process action validates ownership from process status"
if [[ $foreign_uid_test == "checked" ]]; then
pass "process action validates ownership from process status"
else
pass "no foreign userspace process; skipping ownership rejection test"
fi
kernel_thread_pid=""
kernel_thread_start=""
for stat_file in /proc/[0-9]*/stat; do
[[ -r $stat_file ]] || continue
stat_line=$(<"$stat_file") || continue
stat_tail="${stat_line##*) }"
read -r -a stat_fields <<<"$stat_tail"
((${#stat_fields[@]} > 19)) || continue
if ((stat_fields[6] & 0x00200000)); then
kernel_thread_pid="${stat_file%/stat}"
kernel_thread_pid="${kernel_thread_pid##*/}"
kernel_thread_start="${stat_fields[19]}"
break
fi
done
if [[ -n $kernel_thread_pid ]]; then
kernel_result=$("$ROOT/process-signal" TERM "$kernel_thread_pid" "$kernel_thread_start" 2>&1) &&
fail "process action rejects kernel threads"
[[ $kernel_result == *"kernel thread"* ]] ||
fail "process action rejects kernel threads" "$kernel_result"
pass "process action rejects kernel threads"
else
pass "no visible kernel thread; skipping kernel-thread rejection test"
fi
if "$ROOT/process-signal" TERM 1 0 2>/dev/null; then
fail "process action protects PID 1"
fi
pass "process action protects PID 1"
@@ -0,0 +1,872 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const activity = requireFromRoot('Model.js')
const first = activity.parseSnapshot([
'schema\tactivity-resources\t1',
'sample\t1000',
'frequency\tcpu\t2400\tMHz',
'frequency\tmemory\t6400\tMT/s',
'cpu\tcpu\t1000\t700',
'cpu\tcpu0\t500\t350',
'cpu\tcpu1\t500\t350',
'memory\t1000\t400\t200\t150\t125',
'tasks\t2\t100',
'network\teth0\t10000\t20000\tup\t1\t1',
'network\ttailscale0\t500000\t600000\tup\t0\t0',
'disk\t8:0\tsda\t100\t200',
''
].join('\n'))
assertEqual(first.sample, 1000, 'activity parses the monotonic resource sample')
assertEqual(first.cpuFrequencyMHz, 2400, 'activity parses the average current CPU clock')
assertEqual(first.memorySpeedMTs, 6400, 'activity parses the configured DDR transfer rate')
assertEqual(first.cores.length, 2, 'activity parses per-core counters')
assertEqual(first.memory.cached, 125 * 1024, 'activity parses reclaimable cache')
assertEqual(first.memory.swapTotal, 200 * 1024, 'activity parses swap counters')
assertEqual(
activity.parseSnapshot('schema\tactivity-processes\t1\nsample\t1000\n').schema,
0,
'activity rejects a mismatched snapshot contract'
)
const second = activity.parseSnapshot([
'schema\tactivity-resources\t1',
'sample\t1001',
'frequency\tcpu\t2600\tMHz',
'frequency\tmemory\t6400\tMT/s',
'cpu\tcpu\t1200\t800',
'cpu\tcpu0\t600\t390',
'cpu\tcpu1\t600\t410',
'memory\t1000\t350\t200\t100\t120',
'tasks\t3\t110',
'network\teth0\t12048\t21024\tup\t1\t1',
'network\ttailscale0\t900000\t1000000\tup\t0\t0',
'disk\t8:0\tsda\t108\t204',
''
].join('\n'))
const metrics = activity.nextMetrics(first, second, {}, 2)
assertEqual(metrics.cpu, 50, 'activity derives CPU usage from counter deltas')
assertEqual(metrics.memory, 65, 'activity derives memory usage')
assertEqual(metrics.swap, 50, 'activity derives swap usage')
assertEqual(metrics.download, 2048, 'activity derives network receive rate')
assertEqual(metrics.upload, 1024, 'activity derives network transmit rate')
assertEqual(metrics.diskRead, 4096, 'activity derives disk read rate')
assertEqual(metrics.diskWrite, 2048, 'activity derives disk write rate')
assertEqual(metrics.diskDevices.length, 1, 'activity keeps a single disk as one device')
assertEqual(metrics.diskDevices[0].name, 'sda', 'activity names the sampled disk')
assertEqual(activity.diskCycleItems(metrics).length, 1, 'activity does not page a single disk')
assertEqual(metrics.cores[0].usage, 60, 'activity derives per-core usage')
assertEqual(metrics.networkInterface, 'eth0', 'activity follows the default interface without double-counting tunnels')
const firstMultiDisk = activity.parseSnapshot([
'schema\tactivity-resources\t1',
'sample\t2000',
'disk\t8:0\tsda\t100\t200',
'disk\t259:0\tnvme0n1\t50\t80',
''
].join('\n'))
const secondMultiDisk = activity.parseSnapshot([
'schema\tactivity-resources\t1',
'sample\t2001',
'disk\t8:0\tsda\t108\t204',
'disk\t259:0\tnvme0n1\t58\t88',
''
].join('\n'))
const multiDisk = activity.nextMetrics(firstMultiDisk, secondMultiDisk, {}, 4)
assertEqual(multiDisk.diskRead, 8192, 'activity sums read rates across disks')
assertEqual(multiDisk.diskWrite, 6144, 'activity sums write rates across disks')
assertEqual(multiDisk.diskDevices.length, 2, 'activity keeps per-disk rates')
assertEqual(multiDisk.diskDevices[0].name, 'nvme0n1', 'activity sorts disks by name')
assertEqual(multiDisk.diskDevices[0].read, 4096, 'activity derives the first disk read rate')
assertEqual(multiDisk.diskDevices[1].write, 2048, 'activity derives the second disk write rate')
const diskItems = activity.diskCycleItems(multiDisk)
assertEqual(diskItems.length, 3, 'activity prepends an All slot when there are multiple disks')
assertEqual(diskItems[0].id, 'all', 'activity starts the disk pager on the combined total')
assertEqual(diskItems[0].read, 8192, 'activity All slot uses the combined read rate')
assertEqual(activity.selectDiskItem(diskItems, '8:0').name, 'sda', 'activity can select a named disk')
assertEqual(activity.cycleItemId(diskItems, 'all', 'id', 1), '259:0', 'activity cycles from All to the first named disk')
assertEqual(activity.cycleItemId(diskItems, '8:0', 'id', 1), 'all', 'activity wraps the disk pager')
const hotplugDisk = activity.parseSnapshot([
'schema\tactivity-resources\t1',
'sample\t2002',
'disk\t8:0\tsda\t116\t208',
'disk\t8:16\tsdb\t10\t20',
'disk\t259:0\tnvme0n1\t66\t96',
''
].join('\n'))
const afterHotplug = activity.nextMetrics(secondMultiDisk, hotplugDisk, multiDisk, 4)
assertEqual(afterHotplug.diskRead, 8192, 'activity keeps existing disk rates when a disk appears')
assertEqual(afterHotplug.diskDevices.length, 3, 'activity adds a newly attached disk')
assertEqual(
afterHotplug.diskDevices.find(disk => disk.name === 'sdb').read,
0,
'activity baselines a newly attached disk at zero'
)
assertEqual(afterHotplug.diskHistories['259:0'].length, 2, 'activity keeps per-disk sparkline history')
const withHistory = activity.nextMetrics(second, second, {
cpuHistory: [10, 20],
memoryHistory: [30, 40]
}, 2)
assertDeepEqual(withHistory.cpuHistory, [20, 0], 'activity caps history buffers')
assertEqual(activity.counterPercent({ total: 100, idle: 20 }, { total: 50, idle: 10 }), 0, 'activity handles reset CPU counters')
assertEqual(activity.counterRate(100, 50, 1), 0, 'activity handles reset byte counters')
const baseline = activity.baselineMetrics(second, {
cpu: 42,
download: 100,
upload: 50,
diskRead: 25,
diskWrite: 10,
cpuHistory: [20, 42],
memoryHistory: [60, 65]
})
assertEqual(baseline.cpu, 42, 'activity retains the last CPU rate while taking a fresh baseline')
assertEqual(baseline.memory, 65, 'activity immediately refreshes single-sample memory usage')
assertDeepEqual(baseline.cpuHistory, [20, 42], 'activity does not add zeroes while taking a fresh baseline')
const firstProcesses = activity.parseProcessSnapshot([
'schema\tactivity-processes\t1',
'sample\t100\t100',
'process\t42\talice\tS\t1000\t500\t100\tcode helper',
''
].join('\n'))
const secondProcesses = activity.parseProcessSnapshot([
'schema\tactivity-processes\t1',
'sample\t102\t100',
'process\t42\talice\tR\t1000\t600\t100\tcode helper',
''
].join('\n'))
const sampledProcesses = activity.nextProcesses(firstProcesses, secondProcesses, 1000 * 1024)
assertEqual(sampledProcesses[0].name, 'code helper', 'activity preserves process names with spaces')
assertEqual(sampledProcesses[0].user, 'alice', 'activity resolves process users')
assertEqual(sampledProcesses[0].cpu, 50, 'activity derives sampled process CPU')
assertEqual(sampledProcesses[0].memory, 10, 'activity derives process memory share')
assertEqual(sampledProcesses[0].elapsed, 92, 'activity derives process runtime from monotonic uptime')
const reusedPid = activity.parseProcessSnapshot(
'schema\tactivity-processes\t1\nsample\t104\t100\nprocess\t42\talice\tR\t2000\t900\t100\tnew process\n'
)
assertEqual(
activity.nextProcesses(secondProcesses, reusedPid, 1000 * 1024)[0].cpu,
0,
'activity does not carry CPU history across PID reuse'
)
const processes = [
{ pid: 3, name: 'firefox', user: 'alice', cpu: 4, memory: 10, elapsed: 50 },
{ pid: 1, name: 'quickshell', user: 'alice', cpu: 8, memory: 2, elapsed: 100 },
{ pid: 2, name: 'postgres', user: 'postgres', cpu: 1, memory: 20, elapsed: 200 }
]
assertDeepEqual(
activity.filterAndSortProcesses(processes, '', 'cpu', false).map(process => process.pid),
[1, 3, 2],
'activity sorts processes by CPU'
)
assertDeepEqual(
activity.filterAndSortProcesses(processes, 'post', 'memory', false).map(process => process.pid),
[2],
'activity filters processes by name and user'
)
assertEqual(
activity.processIdentityKey({ pid: 42, startTicks: 1234 }),
'42:1234',
'activity ties process selection to PID and sampled start time'
)
assertEqual(
activity.processActionBlockReason(
{ pid: 42, startTicks: 1234, state: 'S', user: 'alice', name: 'firefox' },
'alice'
),
'',
'activity allows an actionable same-user app'
)
assertEqual(
activity.processActionBlockReason(
{ pid: 42, startTicks: 1234, state: 'S', user: 'alice', name: 'quickshell' },
'alice'
),
'Desktop session is protected',
'activity gives immediate feedback for a protected session process'
)
assertEqual(
activity.processActionBlockReason(
{ pid: 42, startTicks: 1234, state: 'S', user: 'root', name: 'worker' },
'alice'
),
'Only your own processes can be ended',
'activity gives immediate feedback for a foreign process'
)
assertEqual(activity.formatBytes(1536, '/s'), '1.50 KiB/s', 'activity formats byte rates')
assertEqual(activity.formatDuration(42), '42s', 'activity formats short process runtimes')
assertEqual(activity.formatDuration(90061), '1d 1h', 'activity formats long durations')
assertEqual(activity.samplingProfile('Efficient').resources, 3000, 'activity offers a low-overhead sampling profile')
assertEqual(activity.samplingProfile('Balanced').processes, 3000, 'activity keeps the balanced process cadence')
assertEqual(activity.samplingProfile('Fast').resources, 750, 'activity offers a responsive sampling profile')
assertEqual(activity.samplingProfile('unknown').name, 'Balanced', 'activity safely defaults unknown sampling profiles')
assertEqual(activity.formatTemperature(20, 'Celsius'), '20°C', 'activity formats Celsius temperatures')
assertEqual(activity.formatTemperature(20, 'Fahrenheit'), '68°F', 'activity converts temperatures to Fahrenheit')
function topo(id, cls, domain, cluster, core) {
return { id, cls, domain, cluster, core, maxKhz: 0 }
}
function usages(count) {
return Array.from({ length: count }, (_, i) => ({ name: 'cpu' + i, usage: i + 1 }))
}
const panther = []
for (let i = 0; i < 4; i++) panther.push(topo(i, 'performance', '0-11', String(i), i))
for (let i = 4; i < 8; i++) panther.push(topo(i, 'efficiency', '0-11', '4-7', i))
for (let i = 8; i < 12; i++) panther.push(topo(i, 'efficiency', '0-11', '8-11', i))
for (let i = 12; i < 16; i++) panther.push(topo(i, 'lowpower', '12-15', '12-15', i))
const pantherDie = activity.coreLayout(usages(16), panther)
assertEqual(pantherDie.mode, 'die', 'activity uses a specialized die for P+E+LP-E')
assertEqual(pantherDie.domains.length, 2, 'activity separates Panther Lake cache domains')
assertDeepEqual(pantherDie.domains[0].rows.map(row => row.kind + ':' + row.cells.length),
['performance:4', 'efficiency:4', 'efficiency:4'],
'activity places 4 P-cores above two E-core rows in the shared-cache square')
assertEqual(pantherDie.domains[1].rows[0].kind, 'lowpower', 'activity keeps LP-E cores in their own square')
assertEqual(pantherDie.domains[1].rows[0].cells.length, 4, 'activity shows four LP-E cores')
const pantherPaint = activity.flattenCoreLayout(pantherDie, {
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
assertEqual(pantherPaint.frames.length, 2, 'activity paints a frame per cache domain')
assertEqual(pantherPaint.frames[0].h, pantherPaint.frames[1].h,
'activity keeps both cache squares the same height')
assertEqual(pantherPaint.frames[1].w < pantherPaint.frames[0].w, true,
'activity rotates the LP-E cache into a narrower column')
assertEqual(pantherPaint.cells.length, 16, 'activity paints every hybrid core as a positioned cell')
assertEqual(pantherPaint.cells[0].size, 9, 'activity draws P-cores larger than E-cores')
assertEqual(pantherPaint.cells[4].size, 6, 'activity draws E-cores at the cluster size')
assertEqual(pantherPaint.cells[4].kind, 'efficiency', 'activity starts the E-core rows after the P-cores')
assertEqual(pantherPaint.cells[8].kind, 'efficiency', 'activity keeps a second E-core row in the shared-cache square')
assertEqual(pantherPaint.cells[12].kind, 'lowpower', 'activity paints LP-E cores after the shared-cache square')
assertEqual(pantherPaint.cells[12].x === pantherPaint.cells[15].x, true,
'activity stacks LP-E cores in a column')
assertEqual(pantherPaint.cells[15].y > pantherPaint.cells[12].y, true,
'activity spreads the LP-E column down the tall cache')
assertEqual(Math.abs(pantherPaint.cells[4].x - pantherPaint.cells[0].x) < 1, true,
'activity does not place the leftmost E-core outside the P-core row')
assertEqual(
Math.abs((pantherPaint.cells[7].x + pantherPaint.cells[7].size)
- (pantherPaint.cells[3].x + pantherPaint.cells[3].size)) < 1,
true,
'activity does not place the rightmost E-core outside the P-core row'
)
assertEqual(pantherPaint.cells[5].x > pantherPaint.cells[4].x, true,
'activity spreads E-cores between the P-core edges')
const lpFrame = pantherPaint.frames[1]
const lpMidX = pantherPaint.cells[12].x + pantherPaint.cells[12].size / 2
assertEqual(Math.abs(lpMidX - (lpFrame.x + lpFrame.w / 2)) < 1, true,
'activity centers LP-E cores horizontally in the small cache')
const lpTop = pantherPaint.cells[12].y - lpFrame.y
const lpBottom = (lpFrame.y + lpFrame.h) - (pantherPaint.cells[15].y + pantherPaint.cells[15].size)
assertEqual(Math.abs(lpTop - lpBottom) <= 1, true,
'activity centers the LP-E column vertically in the small cache')
const paintedAgain = activity.paintCoreLayout(
usages(16).map(core => ({ name: core.name, usage: core.usage + 5 })),
panther,
{ gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7 },
'live'
)
const paintedFirst = activity.paintCoreLayout(
usages(16),
panther,
{ gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7 },
'live'
)
assertEqual(paintedFirst.cells[0].x, paintedAgain.cells[0].x,
'activity reuses die geometry when only usage changes')
assertEqual(paintedFirst.frames, paintedAgain.frames,
'activity keeps cache-domain frames stable across usage samples')
assertEqual(paintedAgain.cells[3].usage > paintedFirst.cells[3].usage, true,
'activity still refreshes per-core usage on a cached die')
const meteor = []
for (let i = 0; i < 6; i++) meteor.push(topo(i, 'performance', '0-13', String(i), i))
for (let i = 6; i < 14; i++) meteor.push(topo(i, 'efficiency', '0-13', i < 10 ? '6-9' : '10-13', i))
for (let i = 14; i < 16; i++) meteor.push(topo(i, 'lowpower', '14-15', '14-15', i))
const meteorDie = activity.coreLayout(usages(16), meteor)
assertEqual(meteorDie.domains[0].rows[0].cells.length, 6, 'activity keeps six Meteor Lake P-cores in one row')
assertEqual(meteorDie.domains[1].rows[0].cells.length, 2, 'activity isolates Meteor Lake LP-E cores')
const meteorPaint = activity.flattenCoreLayout(meteorDie, {
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
const meteorE = meteorPaint.cells.filter(cell => cell.kind === 'efficiency')
assertEqual(meteorE.length, 8, 'activity paints eight Meteor Lake E-cores')
assertEqual(meteorE.every(cell => cell.y === meteorE[0].y), true,
'activity keeps Meteor Lake E-cores on one row under six P-cores')
const alder = []
for (let i = 0; i < 16; i += 2) alder.push(topo(i, 'performance', '0-23', String(i), i), topo(i + 1, 'performance', '0-23', String(i), i))
for (let i = 16; i < 20; i++) alder.push(topo(i, 'efficiency', '0-23', '16-19', i))
for (let i = 20; i < 24; i++) alder.push(topo(i, 'efficiency', '0-23', '20-23', i))
const alderDie = activity.coreLayout(usages(24), alder)
assertEqual(alderDie.mode, 'die', 'activity specializes Alder Lake P+E')
assertEqual(alderDie.domains.length, 1, 'activity keeps a single Alder Lake cache domain together')
assertEqual(alderDie.domains[0].rows[0].cells.length, 8, 'activity collapses SMT siblings into one P-core cell')
assertEqual(alderDie.domains[0].rows[0].cells[0].logicals.length, 2, 'activity tracks both threads inside a P-core cell')
const alderPaint = activity.flattenCoreLayout(alderDie, {
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
const alderE = alderPaint.cells.filter(cell => cell.kind === 'efficiency')
assertEqual(alderE.length, 8, 'activity paints eight Alder Lake E-cores')
assertEqual(alderE.every(cell => cell.y === alderE[0].y), true,
'activity keeps Alder Lake E-cores on one row under eight P-cores')
const raptor = []
for (let i = 0; i < 6; i++) raptor.push(topo(i, 'performance', '0-13', String(i), i))
for (let i = 6; i < 14; i++) raptor.push(topo(i, 'efficiency', '0-13', i < 10 ? '6-9' : '10-13', i))
const raptorPaint = activity.flattenCoreLayout(activity.coreLayout(usages(14), raptor), {
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
const raptorE = raptorPaint.cells.filter(cell => cell.kind === 'efficiency')
assertEqual(raptorE.length, 8, 'activity paints eight Raptor Lake E-cores')
assertEqual(raptorE.every(cell => cell.y === raptorE[0].y), true,
'activity keeps Raptor Lake E-cores on one row under six P-cores')
const lunar = []
for (let i = 0; i < 4; i++) lunar.push(topo(i, 'performance', '0-3', String(i), i))
for (let i = 4; i < 8; i++) lunar.push(topo(i, 'lowpower', '4-7', '4-7', i))
const lunarDie = activity.coreLayout(usages(8), lunar)
assertEqual(lunarDie.domains.length, 2, 'activity splits Lunar Lake P-cores from LP-E')
assertEqual(lunarDie.domains[0].rows[0].kind, 'performance', 'activity shows Lunar Lake P-cores as the primary square')
const lunarPaint = activity.flattenCoreLayout(lunarDie, {
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
assertEqual(lunarPaint.frames[1].y > lunarPaint.frames[0].y, true,
'activity stacks Lunar Lake cache squares instead of placing them in a row')
assertEqual(lunarPaint.frames[1].x >= lunarPaint.frames[0].x, true,
'activity centers the narrower Lunar Lake LP-E square under the P-cores')
const lunarP = lunarPaint.cells.filter(cell => cell.kind === 'performance')
const lunarLp = lunarPaint.cells.filter(cell => cell.kind === 'lowpower')
assertEqual(lunarP.every(cell => cell.y === lunarP[0].y), true,
'activity keeps Lunar Lake P-cores on one row')
assertEqual(lunarLp.every(cell => cell.y === lunarLp[0].y), true,
'activity keeps Lunar Lake LP-E cores on one row')
assertEqual(lunarLp[0].y > lunarP[0].y, true,
'activity places Lunar Lake LP-E under the P-cores')
assertEqual(lunarPaint.width < lunarPaint.frames[0].w + lunarPaint.frames[1].w,
true, 'activity stacks Lunar Lake caches to take less width')
const strix = []
for (let i = 0; i < 4; i++) strix.push(topo(i, 'performance', '0-3', '0-3', i))
for (let i = 4; i < 12; i++) strix.push(topo(i, 'efficiency', '4-11', '4-11', i))
const strixDie = activity.coreLayout(usages(12), strix)
assertEqual(strixDie.domains.length, 2, 'activity splits Strix Point Zen 5 and Zen 5c CCX squares')
assertEqual(strixDie.domains[0].rows[0].cells.length, 4, 'activity shows four Zen 5 cores in the larger square')
assertEqual(strixDie.domains[1].rows.length, 2, 'activity keeps Zen 5c cores as two rows in their CCX')
assertEqual(strixDie.domains[1].rows[0].cells.length, 4, 'activity shows four Zen 5c cores per row')
const strixPaint = activity.flattenCoreLayout(strixDie, {
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
assertEqual(strixPaint.frames[1].y > strixPaint.frames[0].y, true,
'activity stacks Strix Point CCX squares instead of placing them in a row')
const strixP = strixPaint.cells.filter(cell => cell.kind === 'performance')
const strixE = strixPaint.cells.filter(cell => cell.kind === 'efficiency')
assertEqual(strixE[0].y > strixP[0].y, true,
'activity places Strix Point Zen 5c under the Zen 5 cores')
assertEqual(strixPaint.width < strixPaint.frames[0].w + strixPaint.frames[1].w,
true, 'activity stacks Strix Point caches to take less width')
const arm = []
for (let i = 0; i < 1; i++) arm.push(topo(i, 'performance', '0-7', '0', i))
for (let i = 1; i < 4; i++) arm.push(topo(i, 'efficiency', '0-7', '1-3', i))
for (let i = 4; i < 8; i++) arm.push(topo(i, 'lowpower', '0-7', '4-7', i))
const armDie = activity.coreLayout(usages(8), arm)
assertEqual(armDie.domains.length, 1, 'activity keeps a shared-DSU ARM cluster in one square')
assertDeepEqual(armDie.domains[0].rows.map(row => row.kind + ':' + row.cells.length),
['performance:1', 'efficiency:3', 'lowpower:4'],
'activity stacks ARM big.LITTLE tiers by class')
const armPaint = activity.flattenCoreLayout(armDie, {
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
const armP = armPaint.cells.filter(cell => cell.kind === 'performance')
const armE = armPaint.cells.filter(cell => cell.kind === 'efficiency')
const armLp = armPaint.cells.filter(cell => cell.kind === 'lowpower')
assertEqual(armP.length, 1, 'activity paints one ARM big core')
assertEqual(armE.length, 3, 'activity paints three ARM middle cores')
assertEqual(armLp.length, 4, 'activity paints four ARM little cores')
assertEqual(armE.every(cell => cell.y === armE[0].y), true, 'activity keeps ARM middle cores on one row')
assertEqual(armLp.every(cell => cell.y === armLp[0].y), true, 'activity keeps ARM little cores on one row')
assertEqual(armE[0].y > armP[0].y, true, 'activity stacks ARM tiers with the big core on top')
assertEqual(armLp[0].y > armE[0].y, true, 'activity stacks ARM little cores under the middle row')
const armMid = armPaint.frames[0].x + armPaint.frames[0].w / 2
assertEqual(Math.abs(armP[0].x + armP[0].size / 2 - armMid) < 1, true,
'activity centers the ARM big core over the shared DSU')
assertEqual(Math.abs((armLp[0].x + armLp[3].x + armLp[3].size) / 2 - armMid) < 1, true,
'activity centers the ARM little-core row in the shared DSU')
const uniform = Array.from({ length: 16 }, (_, i) => topo(i, 'performance', '0-15', String(i), i))
assertEqual(activity.coreLayout(usages(16), uniform).mode, 'uniform',
'activity keeps a homogeneous desktop CPU on the regular grid')
assertEqual(activity.coreLayout(usages(8), []).mode, 'uniform',
'activity falls back to the regular grid without topology')
const catalog = activity.previewCoreCatalog({
gap: 2, domainGap: 4, pad: 2, performance: 9, efficiency: 6, same: 7
})
assertEqual(catalog.length >= 8, true, 'activity offers a review catalog of CPU layouts')
assertDeepEqual(catalog.map(entry => entry.id), [
'panther-16', 'panther-8', 'meteor-16', 'alder-24', 'raptor-14',
'strix-12', 'arm-8', 'oryon-12', 'desktop-16'
], 'activity previews the researched hybrid and homogeneous layouts')
assertEqual(catalog[0].paint.frames.length, 2, 'activity catalog paints the Panther Lake die')
assertEqual(catalog[7].mode, 'uniform', 'activity catalog keeps Snapdragon X Elite on the regular grid')
const parsedTopo = activity.parseSnapshot([
'schema\tactivity-resources\t1',
'sample\t1',
'cpu-topo\t0\tperformance\t0-11\t0\t0\t4800000',
'cpu-topo\t4\tefficiency\t0-11\t4-7\t4\t3700000',
'cpu-topo\t12\tlowpower\t12-15\t12-15\t12\t3300000',
''
].join('\n'))
assertEqual(parsedTopo.topology.length, 3, 'activity parses cpu-topo lines into the resource snapshot')
assertEqual(parsedTopo.topology[2].cls, 'lowpower', 'activity preserves LP-E classification')
const manifest = requireFromRoot('manifest.json')
const settingKeys = manifest.barWidget.schema.map(entry => entry.key)
assertDeepEqual(
settingKeys,
['samplingSpeed', 'historySamples', 'temperatureUnit', 'showFrequencies', 'openExpanded', 'processPowerEnabled'],
'activity publishes the same focused preferences offered by its settings menu'
)
JS
snapshot=$("$ROOT/activity-stats" --activity-resources)
grep -Eq '^schema[[:space:]]+activity-resources[[:space:]]+1$' <<<"$snapshot" || fail "activity collector emits its schema"
grep -Eq '^sample[[:space:]][0-9]+([.][0-9]+)?$' <<<"$snapshot" || fail "activity collector emits a monotonic sample"
grep -Eq '^cpu[[:space:]]+cpu[[:space:]]' <<<"$snapshot" || fail "activity collector emits CPU counters"
grep -Eq '^memory[[:space:]]' <<<"$snapshot" || fail "activity collector emits memory counters"
grep -Eq '^network[[:space:]]' <<<"$snapshot" || fail "activity collector emits network counters"
grep -Eq '^disk[[:space:]]' <<<"$snapshot" || fail "activity collector emits disk counters"
pass "activity collector emits the resource snapshot contract"
write_cpu_topo() {
local sys="$1" id="$2" core="$3" cluster="$4" l2="$5" l3="$6" khz="$7"
local cpu="$sys/devices/system/cpu/cpu$id"
mkdir -p "$cpu/topology" "$cpu/cache/index2" "$cpu/cpufreq"
printf '%s\n' "$core" >"$cpu/topology/core_id"
printf '%s\n' "$cluster" >"$cpu/topology/cluster_cpus_list"
printf '2\n' >"$cpu/cache/index2/level"
printf 'Unified\n' >"$cpu/cache/index2/type"
printf '%s\n' "$l2" >"$cpu/cache/index2/shared_cpu_list"
if [[ -n $l3 ]]; then
mkdir -p "$cpu/cache/index3"
printf '3\n' >"$cpu/cache/index3/level"
printf 'Unified\n' >"$cpu/cache/index3/type"
printf '%s\n' "$l3" >"$cpu/cache/index3/shared_cpu_list"
fi
printf '%s\n' "$khz" >"$cpu/cpufreq/cpuinfo_max_freq"
}
topo_root=$(mktemp -d)
mkdir -p "$topo_root/sys/devices/cpu_core" "$topo_root/sys/devices/cpu_atom"
printf '0-3\n' >"$topo_root/sys/devices/cpu_core/cpus"
printf '4-15\n' >"$topo_root/sys/devices/cpu_atom/cpus"
for id in 0 1 2 3; do
write_cpu_topo "$topo_root/sys" "$id" "$id" "$id" "$id" "0-11" 4800000
done
for id in 4 5 6 7; do
write_cpu_topo "$topo_root/sys" "$id" "$id" "4-7" "4-7" "0-11" 3700000
done
for id in 8 9 10 11; do
write_cpu_topo "$topo_root/sys" "$id" "$id" "8-11" "8-11" "0-11" 3700000
done
for id in 12 13 14 15; do
write_cpu_topo "$topo_root/sys" "$id" "$id" "12-15" "12-15" "" 3300000
done
topo_snapshot=$(
OMARCHY_SYSTEM_STATS_SYS_PATH="$topo_root/sys" \
"$ROOT/activity-stats" --activity-resources
)
rm -rf -- "$topo_root"
[[ $(grep -c $'^cpu-topo\t' <<<"$topo_snapshot") -eq 16 ]] ||
fail "activity collector does not emit a topology row for every logical CPU"
[[ $(grep -c $'^cpu-topo\t[0-9]\tperformance\t0-11\t' <<<"$topo_snapshot") -eq 4 ]] ||
fail "activity collector does not classify Intel P-cores"
[[ $(grep -c $'\tefficiency\t0-11\t' <<<"$topo_snapshot") -eq 8 ]] ||
fail "activity collector does not classify Intel E-cores that share the P-core L3"
[[ $(grep -c $'\tlowpower\t12-15\t' <<<"$topo_snapshot") -eq 4 ]] ||
fail "activity collector does not isolate LP-E cores without that L3"
pass "activity collector classifies hybrid CPU cache domains"
gpu_snapshot=$("$ROOT/activity-stats" --activity-gpus)
grep -Eq '^schema[[:space:]]+activity-gpus[[:space:]]+1$' <<<"$gpu_snapshot" ||
fail "activity GPU collector emits its schema"
grep -Eq '^sample[[:space:]][0-9]+([.][0-9]+)?$' <<<"$gpu_snapshot" ||
fail "activity GPU collector emits a monotonic sample"
pass "activity collector emits the independent GPU snapshot contract"
process_snapshot=$("$ROOT/activity-stats" --activity-processes)
grep -Eq '^schema[[:space:]]+activity-processes[[:space:]]+1$' <<<"$process_snapshot" || fail "activity process collector emits its schema"
grep -Eq '^sample[[:space:]][0-9]+([.][0-9]+)?[[:space:]][0-9]+[[:space:]][0-9]+$' <<<"$process_snapshot" ||
fail "activity process collector emits its sample and CPU clocks"
grep -Eq '^process[[:space:]]' <<<"$process_snapshot" || fail "activity process collector emits process rows"
pass "activity collector emits the process snapshot contract"
[[ -x $ROOT/activity-sampler ]] || fail "native activity sampler is executable"
[[ $("$ROOT/activity-sampler" --version) == "activity-sampler 2.1.1" ]] ||
fail "native activity sampler reports the release protocol version"
[[ ! -e $ROOT/activity-process-stats ]] ||
fail "legacy process helper remains beside the unified native sampler"
pass "activity ships one native sampler instead of a process-helper chain"
reader_snapshot=$(
printf 'resources\nprocesses\nprocesses\nthermals\ngpus\nstorage\n' |
"$ROOT/activity-stats" --activity-reader
)
[[ $(grep -c '^snapshot-end' <<<"$reader_snapshot") -eq 6 ]] ||
fail "activity reader does not frame every requested snapshot"
grep -Fq $'snapshot-end\tresources' <<<"$reader_snapshot" ||
fail "activity reader does not frame resource snapshots"
grep -Fq $'snapshot-end\tprocesses' <<<"$reader_snapshot" ||
fail "activity reader does not frame process snapshots"
[[ $(grep -c $'^snapshot-end\tprocesses$' <<<"$reader_snapshot") -eq 2 ]] ||
fail "activity reader does not serve repeated process snapshots"
grep -Fq $'snapshot-end\tthermals' <<<"$reader_snapshot" ||
fail "activity reader does not frame thermal snapshots"
grep -Fq $'snapshot-end\tgpus' <<<"$reader_snapshot" ||
fail "activity reader does not frame GPU snapshots"
grep -Fq $'snapshot-end\tstorage' <<<"$reader_snapshot" ||
fail "activity reader does not frame storage snapshots"
pass "activity reader serves independently requested snapshots in one native process"
coproc SELF_READER { exec "$ROOT/activity-stats" --activity-reader; }
self_reader_pid=$SELF_READER_PID
self_reader_input=${SELF_READER[1]}
self_reader_output=${SELF_READER[0]}
self_reader_snapshot=""
printf 'processes\n' >&"$self_reader_input"
while IFS= read -r -u "$self_reader_output" line; do
[[ $line == $'snapshot-end\tprocesses' ]] && break
self_reader_snapshot+="$line"$'\n'
done
process_children=$(<"/proc/$self_reader_pid/task/$self_reader_pid/children")
[[ -z $process_children ]] ||
fail "native activity reader spawned a child collector"
printf 'processes\n' >&"$self_reader_input"
while IFS= read -r -u "$self_reader_output" line; do
[[ $line == $'snapshot-end\tprocesses' ]] && break
done
process_children=$(<"/proc/$self_reader_pid/task/$self_reader_pid/children")
[[ -z $process_children ]] ||
fail "native activity reader spawned a child between process snapshots"
exec {self_reader_input}>&-
wait "$self_reader_pid"
pass "activity reader keeps all unprivileged sampling in one native process"
if grep -Fq $'process\t'"$self_reader_pid"$'\t' <<<"$self_reader_snapshot"; then
fail "activity reader reports its own sampling process as workload"
fi
pass "activity reader excludes its own sampling process"
fixture_root=$(mktemp -d)
trap 'rm -rf -- "$fixture_root"' EXIT
mkdir -p "$fixture_root/proc/101" "$fixture_root/proc/102" "$fixture_root/proc/103"
printf '200.00 100.00\n' >"$fixture_root/proc/uptime"
printf '%s\n' \
'101 (worker task) R 1 1 1 0 -1 0 0 0 0 0 12 8 0 0 20 0 3 0 1000 4096000 25' \
>"$fixture_root/proc/101/stat"
printf 'Name:\tworker task\nUid:\t1000\t1000\t1000\t1000\nVmRSS:\t120 kB\n' \
>"$fixture_root/proc/101/status"
printf '%s\n' \
'103 (kernel worker) I 2 0 0 0 0 2097152 0 0 0 0 0 0 0 0 20 0 1 0 1000 0 0' \
>"$fixture_root/proc/103/stat"
printf 'alice:x:1000:1000:Alice:/home/alice:/bin/bash\n' >"$fixture_root/passwd"
fixture_process_snapshot=$(
OMARCHY_SYSTEM_STATS_PROC_PATH="$fixture_root/proc" \
OMARCHY_SYSTEM_STATS_PASSWD_PATH="$fixture_root/passwd" \
OMARCHY_SYSTEM_STATS_CLOCK_TICKS=100 \
OMARCHY_SYSTEM_STATS_PAGE_SIZE=4096 \
"$ROOT/activity-stats" --activity-processes
)
grep -Fq $'process\t101\talice\tR\t1000\t20\t100\tworker task' \
<<<"$fixture_process_snapshot" || fail "activity process collector reads a stable fixture process"
[[ $(grep -c '^process' <<<"$fixture_process_snapshot") -eq 1 ]] ||
fail "activity process collector did not skip a vanished fixture PID"
pass "activity process collector skips PIDs that vanish during a snapshot"
if grep -Fq $'process\t103\t' <<<"$fixture_process_snapshot"; then
fail "activity process collector included a kernel thread"
fi
pass "activity process collector omits non-actionable kernel threads"
grep -Fq 'centerOnBar: false' "$ROOT/Panel.qml" ||
fail "activity panel keeps compact and expanded layouts on the same bar anchor"
pass "activity panel expansion preserves its bar anchor"
grep -Fq 'implicitWidth: button.implicitWidth' "$ROOT/Panel.qml" ||
fail "activity bar widget does not expose its icon width"
grep -Fq 'implicitHeight: button.implicitHeight' "$ROOT/Panel.qml" ||
fail "activity bar widget does not expose its icon height"
pass "activity bar widget exposes its icon geometry"
grep -Fq 'id: headerActions' "$ROOT/Panel.qml" ||
fail "activity header actions do not share a header row"
grep -A3 -F 'id: headerActions' "$ROOT/Panel.qml" |
grep -Fq 'anchors.verticalCenter: parent.verticalCenter' ||
fail "activity header controls are not vertically centered together"
pass "activity header actions stay aligned"
panel_file="$ROOT/Panel.qml"
controller_file="$ROOT/ActivityController.qml"
action_controller_file="$ROOT/ProcessActionController.qml"
if grep -Eq 'label: "LOAD"|detail: .*"LOAD ' "$panel_file"; then
fail "activity panel exposes the Linux load average"
fi
if grep -Fq 'LOGICAL CPUS' "$panel_file"; then
fail "activity panel still advertises logical CPU count as card filler"
fi
grep -Fq 'function cpuCardDetail()' "$panel_file" &&
grep -Fq 'Model.formatTemperature(cpuTemperature.value, temperatureUnitSetting)' "$panel_file" ||
fail "activity CPU card does not own the CPU temperature"
if grep -Fq 'component TemperatureBadge' "$panel_file"; then
fail "activity temperature still lives in the header"
fi
pass "activity keeps CPU temperature on the CPU card"
grep -Fq 'label: root.storageCardLabel()' "$panel_file" ||
fail "activity expanded details do not show disk storage"
grep -Fq 'label: root.gpuHeadingText()' "$panel_file" &&
grep -Fq 'gpuUsageText(adapters[0])' "$panel_file" ||
fail "activity expanded details do not show GPU adapters"
grep -Fq 'return "Shared"' "$panel_file" ||
fail "activity panel does not distinguish integrated shared GPU memory"
grep -Fq 'return "VRAM"' "$panel_file" ||
fail "activity panel does not label dedicated GPU memory"
grep -Fq 'function storageCardValue()' "$panel_file" &&
grep -Fq 'function storageCardDetail()' "$panel_file" &&
grep -Fq '" free"' "$panel_file" ||
fail "activity storage card does not show used storage and free space"
grep -Fq 'function cycleDisk(' "$panel_file" &&
grep -Fq 'function cycleStorage(' "$panel_file" &&
grep -Fq 'interactive: root.canCycleDisks' "$panel_file" &&
grep -Fq 'interactive: root.canCycleStorage' "$panel_file" &&
grep -Fq 'raw === "D" ? -1 : 1' "$panel_file" &&
grep -Fq 'raw === "V" ? -1 : 1' "$panel_file" &&
grep -Fq 'd/v cycle drives' "$panel_file" ||
fail "activity does not page Disk and Storage with click and d/v"
grep -Fq 'resetDriveSelection()' "$panel_file" ||
fail "activity does not reset drive selection when the panel closes"
if grep -Fq 'POWER & THERMALS' "$panel_file"; then
fail "activity expanded details still show power and thermals"
fi
if grep -Fq 'label: "RAM TOTAL"' "$panel_file" || grep -Fq 'label: "CORES"' "$panel_file"; then
fail "activity GPU card is still padded with unrelated system facts"
fi
pass "activity shows GPU and storage without filler metrics"
grep -Fq 'ActivityController {' "$panel_file" &&
grep -Fq 'active: root.opened' "$panel_file" &&
grep -Fq 'expanded: root.expanded' "$panel_file" ||
fail "activity panel does not delegate sampling lifecycle to its controller"
grep -Fq 'readonly property var snapshot: _snapshot' "$controller_file" &&
grep -Fq 'readonly property var processes: _processes' "$controller_file" ||
fail "activity controller does not expose read-only sampled state"
if grep -Eq '^[[:space:]]*Process[[:space:]]*[{]' "$panel_file"; then
fail "activity visual panel still owns a helper process"
fi
grep -Fq 'command: [root.statsHelperPath, "--activity-reader"]' "$controller_file" ||
fail "activity panel does not reuse a single unprivileged stats reader"
grep -Fq 'String(Qt.resolvedUrl("activity-sampler"))' "$controller_file" ||
fail "activity panel does not launch the native sampler directly"
grep -Fq '? [powerHelperPath]' "$controller_file" ||
fail "activity panel does not use the persistent guarded CPU-energy reader"
grep -Fq 'if (!active || !expanded || !processPowerEnabled' "$controller_file" &&
grep -Fq '|| processPowerUnavailable || _processPowerSamplePending) return' "$controller_file" ||
fail "activity panel samples process power outside the advanced view"
grep -Fq 'onExpandedChanged:' "$controller_file" &&
grep -Fq 'resetProcessPowerMeasurement()' "$controller_file" ||
fail "activity panel carries an energy baseline across collapsed time"
grep -Fq 'root.markProcessPowerUnavailable()' "$controller_file" ||
fail "activity panel does not clear stale watts after an energy read failure"
grep -Fq 'refreshProcessCycle()' "$controller_file" ||
fail "activity panel does not coordinate energy and process refreshes"
grep -Fq 'id: deadlineScheduler' "$controller_file" &&
grep -Fq 'function collectDueSnapshots()' "$controller_file" &&
grep -Fq '_resourceDue = nextDeadline(_resourceDue, refreshInterval, now)' "$controller_file" &&
grep -Fq '_processDue = nextDeadline(_processDue, processRefreshInterval, now)' "$controller_file" &&
grep -Fq '_thermalDue = nextDeadline(_thermalDue, thermalRefreshInterval, now)' "$controller_file" &&
grep -Fq '_gpuDue = nextDeadline(_gpuDue, gpuRefreshInterval, now)' "$controller_file" &&
grep -Fq '_storageDue = nextDeadline(_storageDue, storageRefreshInterval, now)' "$controller_file" ||
fail "activity panel does not coalesce its independent deadlines"
if grep -Fq 'repeat: true' "$controller_file"; then
fail "activity panel still wakes separate recurring timers"
fi
grep -Fq 'id: statsReaderWatchdog' "$controller_file" &&
grep -Fq 'id: processPowerWatchdog' "$controller_file" ||
fail "activity readers can remain stuck indefinitely"
grep -Fq 'powerStateSwitchingRoot: "switching-root"' "$controller_file" &&
grep -Fq 'readonly property string processPowerState: _processPowerState' "$controller_file" ||
fail "activity power reader lifecycle is not represented by an explicit state"
if grep -Eq 'processPowerRestartWithSudo|processPowerUseSudo|processPowerReaderReady[[:space:]]*:' "$controller_file"; then
fail "activity power reader still uses overlapping lifecycle booleans"
fi
if grep -Eq 'activity-package-power|refreshPackagePower|packagePowerProc' "$controller_file"; then
fail "activity panel still polls package power"
fi
grep -Fq 'if (expanded) sendStatsRequest("gpus")' "$controller_file" &&
grep -Fq '_gpuDue = 0' "$controller_file" &&
grep -Fq 'resetGpuMeasurement()' "$controller_file" ||
fail "activity panel samples GPU details while the advanced view is collapsed"
grep -Fq '_metrics = Model.baselineMetrics(next, metrics)' "$controller_file" &&
grep -Fq 'function resetSessionSampling()' "$controller_file" &&
grep -Fq 'if (_gpus.length === 0)' "$controller_file" ||
fail "activity panel does not retain valid display data while taking fresh baselines"
if grep -Fq ' _snapshot = Model.emptySnapshot()' "$controller_file" ||
grep -Fq ' _processes = []' "$controller_file" ||
grep -Fq ' _gpus = []' "$controller_file"; then
fail "activity panel clears stale display data when a sampling session changes"
fi
pass "activity controller coalesces deadlines and refreshes retained values without blanking"
grep -Fq 'label: "~W"' "$panel_file" ||
fail "activity process table does not label estimated CPU watts"
grep -Fq 'measuredPackagePowerText(processPower.watts)' "$panel_file" ||
fail "activity CPU summary does not expose measured total CPU-package power"
grep -Fq 'return "~" + watts.toFixed' "$panel_file" ||
fail "activity process watts are not visibly marked as estimates"
grep -Fq 'root.setSort("power")' "$panel_file" ||
fail "activity process power cannot be sorted"
grep -Fq 'if (nextKey === "power" && (!powerEstimatesEnabled || !processPower.available)) return' "$panel_file" ||
fail "activity process power sorting remains active without a measurement"
grep -Fq 'showProcessUserColumn' "$panel_file" &&
grep -Fq 'showProcessTimeColumn' "$panel_file" ||
fail "activity process table does not adapt its optional columns on narrow screens"
grep -Fq 'model: root.sortedProcesses' "$panel_file" ||
fail "activity process table silently caps the virtualized process list"
grep -Fq 'Model.formatBytes(snapshot.memory.cached)' "$panel_file" ||
fail "activity panel does not expose reclaimable memory cache"
[[ $(grep -Fc 'label: root.memoryCardLabel()' "$panel_file") -eq 2 ]] ||
fail "activity panel does not place RAM speed in both card headings"
[[ $(grep -Fc 'label: root.cpuCardLabel()' "$panel_file") -eq 2 ]] ||
fail "activity panel does not place CPU frequency in both card headings"
grep -Fq 'function memoryBreakdownText()' "$panel_file" &&
grep -Fq 'if (usedUnit && usedUnit === cacheUnit)' "$panel_file" &&
grep -Fq '" · cache " + cacheText' "$panel_file" ||
fail "activity RAM card does not show used memory and cache together"
grep -Fq 'snapshot.memorySpeedMTs, "MT/s"' "$panel_file" &&
grep -Fq 'snapshot.cpuFrequencyMHz, "MHz"' "$panel_file" &&
grep -Fq 'var frequency = gpuFrequencyText(adapters[0])' "$panel_file" &&
grep -Fq 'if (frequency === 0) return "IDLE"' "$panel_file" ||
fail "activity card headings do not distinguish DDR transfer rate from CPU and GPU clocks"
[[ $(grep -Fc 'badge: root.swapBadgeText()' "$panel_file") -eq 2 ]] &&
grep -Fq 'return "Swap " + percentText(metrics.swap)' "$panel_file" ||
fail "activity RAM cards do not expose swap usage"
if grep -Fq 'label: "DISK"' "$panel_file"; then
fail "activity disk card still shouts its heading"
fi
if grep -Fq 'component CoreLine' "$panel_file"; then
fail "activity still draws a labeled bar for every logical core"
fi
grep -Fq 'component CoreHeatGrid' "$panel_file" &&
grep -Fq 'paint: root.corePaintCompact' "$panel_file" &&
grep -Fq 'paint: root.corePaintExpanded' "$panel_file" &&
grep -Fq 'corePaintSizesExpanded' "$panel_file" &&
grep -Fq 'id: coreDie' "$panel_file" ||
fail "activity does not draw per-core load as a die on the CPU card"
if grep -Fq 'text: "Cores"' "$panel_file"; then
fail "activity still treats cores as a separate strip under the cards"
fi
grep -Fq 'component ProcessSortCell' "$panel_file" &&
grep -Fq 'sortKey: "cpu"' "$panel_file" &&
grep -Fq 'sortKey: "runtime"' "$panel_file" ||
fail "activity process column headers are not the sort controls"
if grep -Fq 'text: "More details"' "$panel_file"; then
fail "activity compact view still duplicates the expand control"
fi
grep -Fq 'property bool hintsVisible: false' "$panel_file" &&
grep -Fq 'function shortcutHintText()' "$panel_file" ||
fail "activity keyboard hints are not opt-in"
grep -Fq 'contentHeight: panel.fittedContentHeight(' "$panel_file" &&
grep -Fq 'contentLoader.item.implicitHeight' "$panel_file" ||
fail "activity panel does not size the window to its content"
if grep -Fq 'processFooterReserve' "$panel_file"; then
fail "activity still shrinks the process list to make room for keyboard hints"
fi
if grep -Fq 'Style.space(720)' "$panel_file" || grep -Fq 'Style.space(560))' "$panel_file"; then
fail "activity still caps the window below the height of keyboard hints"
fi
grep -Fq 'height: Style.space(300)' "$panel_file" ||
fail "activity expanded process list is not a stable height while hints open"
if grep -Fq 'Preview CPU layouts' "$panel_file" || grep -Fq 'layoutPreviewContent' "$panel_file"; then
fail "activity still ships the CPU layout preview in the production panel"
fi
if grep -Fq 'label: "GPU"' "$panel_file"; then
fail "activity compact view still shows a GPU row"
fi
grep -Fq 'id: settingsContent' "$panel_file" &&
grep -Fq 'label: "Update speed"' "$panel_file" &&
grep -Fq 'label: "Graph history"' "$panel_file" &&
grep -Fq 'label: "Temperature"' "$panel_file" &&
grep -Fq 'function persistSettings(values)' "$panel_file" &&
grep -Fq 'root.bar.shell.updateEntryInline(root.moduleName, entry)' "$panel_file" ||
fail "activity panel does not expose persistent personalization controls"
grep -Fq 'processPowerEnabled: root.powerEstimatesEnabled' "$panel_file" &&
grep -Fq 'onProcessPowerEnabledChanged:' "$controller_file" &&
grep -Fq 'visible: !compact && root.powerEstimatesEnabled' "$panel_file" ||
fail "activity power preference does not stop sampling and simplify the process table"
if grep -Fq 'text: "POWER"' "$panel_file"; then
fail "activity presents estimated per-process power as an exact measurement"
fi
pass "activity clearly labels and sorts estimated per-process CPU power"
grep -Fq 'if (cursorActive && selectedProcessIndex === index) return' "$panel_file" ||
fail "activity process rows remap pointer coordinates after already being selected"
grep -Fq 'hoverEnabled: !compact' "$panel_file" ||
fail "activity compact process rows subscribe to unused hover movement"
if grep -Fq 'animateCursor: true' "$panel_file"; then
fail "activity process row selection starts multi-frame hover animations"
fi
pass "activity process rows avoid input-rate pointer and animation work"
grep -Fq 'ConfirmDialog {' "$panel_file" ||
fail "activity process actions are not confirmed"
grep -Fq 'processConfirm.selectedIndex = 0' "$panel_file" ||
fail "activity process confirmation does not default to Cancel"
grep -Fq 'if (!root.settingsOpen && !processActions.confirmationOpen && root.cursorActive)' "$panel_file" ||
fail "activity process shortcut can act without an explicit selection"
grep -Fq 'ProcessActionController {' "$panel_file" &&
grep -Fq 'active: root.opened' "$panel_file" &&
grep -Fq 'enabled: !root.settingsOpen' "$panel_file" ||
fail "activity panel does not delegate guarded app actions to their controller"
grep -Fq 'function requestCloseProcess(process, index)' "$panel_file" &&
grep -Fq 'onClicked: root.requestCloseProcess(parent.processData, compact ? -1 : parent.rowIndex)' "$panel_file" ||
fail "activity process rows do not confirm close on left click"
if grep -Fq 'id: endProcessButton' "$panel_file" || grep -Fq 'id: endProcessHost' "$panel_file"; then
fail "activity still shows a separate End app button"
fi
grep -Fq 'Model.processIdentityKey' "$panel_file" ||
fail "activity process selection is not tied to its sampled start time"
grep -Fq 'signalHelperPath,' "$action_controller_file" ||
fail "activity panel does not use the guarded process signal helper"
grep -Fq 'action: "APP_TERM"' "$action_controller_file" ||
fail "activity panel does not resolve worker processes to their parent app"
grep -Fq 'confirmText: "Close"' "$panel_file" ||
fail "activity panel does not confirm closing the selected process"
grep -Fq 'Do you want to close ' "$panel_file" ||
fail "activity panel does not ask a simple close confirmation"
grep -Fq '_status = reason' "$action_controller_file" ||
fail "activity panel does not explain why a process cannot be closed"
grep -Fq '"hyprland"' "$ROOT/Model.js" ||
fail "activity panel does not protect the desktop compositor"
grep -Fq '"systemd-executo"' "$ROOT/Model.js" ||
fail "activity panel does not protect the kernel-truncated systemd executor name"
grep -Fq 'disposition === "graceful"' "$action_controller_file" &&
grep -Fq 'disposition === "escalated"' "$action_controller_file" ||
fail "activity app action status does not distinguish normal and forced closure"
if grep -Eq 'bash[[:space:]]+-c|pkexec' "$panel_file" "$action_controller_file"; then
fail "activity process actions invoke a shell or privilege prompt"
fi
pass "activity app actions use guarded identity and safe confirmation"
@@ -0,0 +1,14 @@
#!/bin/bash
set -euo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
for test_file in \
activity-test.sh \
activity-details-test.sh \
activity-native-test.sh \
activity-privilege-test.sh \
activity-process-action-test.sh; do
"$root/test/$test_file"
done
@@ -0,0 +1,80 @@
#!/bin/bash
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
echo "source test/shell.d/base-test.sh from a shell test; do not run it directly" >&2
exit 1
fi
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
SHELL_TEST_DIR="$ROOT/test"
export ROOT
pass() {
printf 'ok - %s\n' "$1"
}
fail() {
local description="$1"
local detail="${2:-}"
[[ -n $detail ]] && printf '%s\n' "$detail" >&2
printf 'not ok - %s\n' "$description" >&2
exit 1
}
require_command() {
local command="$1"
command -v "$command" >/dev/null || fail "required command is available: $command"
}
run_node_test() {
require_command node
{
cat <<'JS_PRELUDE'
const path = require('path')
const root = process.env.ROOT
function fail(description, detail) {
if (detail) console.error(detail)
console.error(`not ok - ${description}`)
process.exit(1)
}
function pass(description) {
console.log(`ok - ${description}`)
}
function assert(condition, description, detail) {
if (!condition) fail(description, detail)
pass(description)
}
function assertEqual(actual, expected, description) {
assert(
actual === expected,
description,
`expected: ${expected}\nactual: ${actual}`
)
}
function assertDeepEqual(actual, expected, description) {
const actualJson = JSON.stringify(actual)
const expectedJson = JSON.stringify(expected)
assert(
actualJson === expectedJson,
description,
`expected: ${expectedJson}\nactual: ${actualJson}`
)
}
function requireFromRoot(relativePath) {
return require(path.join(root, relativePath))
}
JS_PRELUDE
cat
} | node
}