diff --git a/Modules/RAM/readers.swift b/Modules/RAM/readers.swift index 5d89bb95..3772c2b5 100644 --- a/Modules/RAM/readers.swift +++ b/Modules/RAM/readers.swift @@ -107,11 +107,15 @@ public class ProcessReader: Reader<[TopProcess]> { private let title: String = "RAM" private var numberOfProcesses: Int { - get { - return Store.shared.int(key: "\(self.title)_processes", defaultValue: 8) - } + get { Store.shared.int(key: "\(self.title)_processes", defaultValue: 8) } } + private var combinedProcesses: Bool{ + get { Store.shared.bool(key: "\(self.title)_combinedProcesses", defaultValue: false) } + } + + private typealias dynGetResponsiblePidFuncType = @convention(c) (CInt) -> CInt + public override func setup() { self.popup = true self.setInterval(Store.shared.int(key: "\(self.title)_updateTopInterval", defaultValue: 1)) @@ -124,7 +128,11 @@ public class ProcessReader: Reader<[TopProcess]> { let task = Process() task.launchPath = "/usr/bin/top" - task.arguments = ["-l", "1", "-o", "mem", "-n", "\(self.numberOfProcesses)", "-stats", "pid,command,mem"] + if self.combinedProcesses { + task.arguments = ["-l", "1", "-o", "mem", "-stats", "pid,command,mem"] + } else { + task.arguments = ["-l", "1", "-o", "mem", "-n", "\(self.numberOfProcesses)", "-stats", "pid,command,mem"] + } let outputPipe = Pipe() let errorPipe = Pipe() @@ -157,7 +165,64 @@ public class ProcessReader: Reader<[TopProcess]> { } } - self.callback(processes) + if !self.combinedProcesses { + self.callback(processes) + return + } + + var processGroups: [String: [TopProcess]] = [:] + for process in processes { + let responsiblePid = ProcessReader.getResponsiblePid(process.pid) + let groupKey = "\(responsiblePid)" + + if processGroups[groupKey] != nil { + processGroups[groupKey]!.append(process) + } else { + processGroups[groupKey] = [process] + } + } + + var result: [TopProcess] = [] + for (_, processes) in processGroups { + let totalUsage = processes.reduce(0) { $0 + $1.usage } + let firstProcess = processes.first! + let name: String + + if let app = NSRunningApplication(processIdentifier: pid_t(ProcessReader.getResponsiblePid(firstProcess.pid))), + let appName = app.localizedName { + name = appName + } else { + name = firstProcess.name + } + + result.append(TopProcess( + pid: ProcessReader.getResponsiblePid(firstProcess.pid), + name: name, + usage: totalUsage + )) + } + + result.sort { $0.usage > $1.usage } + self.callback(Array(result.prefix(self.numberOfProcesses))) + } + + private static let dynGetResponsiblePidFunc: UnsafeMutableRawPointer? = { + let result = dlsym(UnsafeMutableRawPointer(bitPattern: -1), "responsibility_get_pid_responsible_for_pid") + if result == nil { + error("Error loading responsibility_get_pid_responsible_for_pid") + } + return result + }() + + static func getResponsiblePid(_ childPid: Int) -> Int { + guard ProcessReader.dynGetResponsiblePidFunc != nil else { + return childPid + } + let responsiblePid = unsafeBitCast(ProcessReader.dynGetResponsiblePidFunc, to: dynGetResponsiblePidFuncType.self)(CInt(childPid)) + guard responsiblePid != -1 else { + return childPid + } + return Int(responsiblePid) } static public func parseProcess(_ raw: String) -> TopProcess { diff --git a/Modules/RAM/settings.swift b/Modules/RAM/settings.swift index f74318d0..61266266 100644 --- a/Modules/RAM/settings.swift +++ b/Modules/RAM/settings.swift @@ -49,6 +49,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { private var splitValueState: Bool = false private var notificationLevel: String = "Disabled" private var textValue: String = "$mem.used/$mem.total ($pressure.value)" + private var combinedProcessesState: Bool = false private let title: String @@ -67,6 +68,7 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { self.splitValueState = Store.shared.bool(key: "\(self.title)_splitValue", defaultValue: self.splitValueState) self.notificationLevel = Store.shared.string(key: "\(self.title)_notificationLevel", defaultValue: self.notificationLevel) self.textValue = Store.shared.string(key: "\(self.title)_textWidgetValue", defaultValue: self.textValue) + self.combinedProcessesState = Store.shared.bool(key: "\(self.title)_combinedProcesses", defaultValue: self.combinedProcessesState) super.init(frame: NSRect.zero) @@ -96,6 +98,10 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { ])) self.addArrangedSubview(PreferencesSection([ + PreferencesRow(localizedString("Combined processes"), component: switchView( + action: #selector(toggleCombinedProcesses), + state: self.combinedProcessesState + )), PreferencesRow(localizedString("Number of top processes"), component: selectView( action: #selector(changeNumberOfProcesses), items: NumbersOfProcesses.map{ KeyValue_t(key: "\($0)", value: "\($0)") }, @@ -163,6 +169,11 @@ internal class Settings: NSStackView, Settings_v, NSTextFieldDelegate { Store.shared.set(key: "\(self.title)_splitValue", value: self.splitValueState) self.callback() } + @objc private func toggleCombinedProcesses(_ sender: NSControl) { + self.combinedProcessesState = controlState(sender) + Store.shared.set(key: "\(self.title)_combinedProcesses", value: self.combinedProcessesState) + self.callback() + } func controlTextDidChange(_ notification: Notification) { if let field = notification.object as? NSTextField {