Commitok összehasonlítása
2 Commit-ok
6a72c7ba64
...
c9cd87f221
| Szerző | SHA1 | Dátum | |
|---|---|---|---|
|
|
c9cd87f221 | ||
|
|
6f60dd7ad8 |
@@ -10,6 +10,7 @@
|
||||
00EC5A90B7B8C6C5015D90D1 /* PerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80A817DCDA254349DEA6F06 /* PerformanceView.swift */; };
|
||||
0AA7D351AEB05078FA9B4B93 /* ProcessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6F97E6EF65AA17A469F037 /* ProcessView.swift */; };
|
||||
136DAD1FE90FD682DF666667 /* TopManager_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */; };
|
||||
14D77C268F9F701E612E4FD3 /* WindowVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A1CB7CF2D7EA7A20E561192 /* WindowVisibility.swift */; };
|
||||
1F212CE158B7D14B29077B18 /* CPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44281079654C7E1C9E16806C /* CPUInfo.swift */; };
|
||||
1F25A84B560709AE71C27D23 /* GPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F481045B4FC6017F50849FF1 /* GPUInfo.swift */; };
|
||||
24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */; };
|
||||
@@ -35,6 +36,7 @@
|
||||
9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA15E30C4B694CB36DC0893 /* TrendChartView.swift */; };
|
||||
A0649565A902BA48DB10F592 /* CPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ED582C412C63A0A2E2103E /* CPUMonitor.swift */; };
|
||||
A7EC9FB55D5088D669491C11 /* AppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813543165B39EBD213EB3BC7 /* AppsView.swift */; };
|
||||
AE29356C1D41F33A3E94B6B7 /* MenuBarStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B37A9AE9F6215C0C132B8D9 /* MenuBarStatus.swift */; };
|
||||
B0165358B483B0ECC28DB4DE /* PowerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C512B13DC0C1024CF39A5E /* PowerInfo.swift */; };
|
||||
B62B18DF32A8734D5908B16B /* DiskInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */; };
|
||||
B89695749D32F394DB459C15 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACBF14FFC5EC68C9E438040 /* SettingsView.swift */; };
|
||||
@@ -43,6 +45,7 @@
|
||||
CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFADE83965AA863B775A910 /* NetworkChartView.swift */; };
|
||||
D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A404EBD588B7ACD36079A95D /* TopManagerApp.swift */; };
|
||||
D9E882888E23C26B748E22CF /* PowerMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */; };
|
||||
DA03FF0D25DB8B9D3B09BC82 /* MenuBarStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E44036C6DB5C055C179A3E2 /* MenuBarStatusTests.swift */; };
|
||||
DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279B972C08A878053821285D /* SystemMonitor.swift */; };
|
||||
DE50ECC69FCE60B10364802C /* ByteFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C9B469F6A45F3CC21A7594 /* ByteFormatters.swift */; };
|
||||
E379C349DC9617D6C505B74B /* GPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */; };
|
||||
@@ -87,6 +90,7 @@
|
||||
44281079654C7E1C9E16806C /* CPUInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUInfo.swift; sourceTree = "<group>"; };
|
||||
4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; };
|
||||
51C512B13DC0C1024CF39A5E /* PowerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerInfo.swift; sourceTree = "<group>"; };
|
||||
5B37A9AE9F6215C0C132B8D9 /* MenuBarStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarStatus.swift; sourceTree = "<group>"; };
|
||||
5E1D2EA8E1AD11B42FF225EB /* TopManager.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TopManager.entitlements; sourceTree = "<group>"; };
|
||||
70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryMathTests.swift; sourceTree = "<group>"; };
|
||||
7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryLineChart.swift; sourceTree = "<group>"; };
|
||||
@@ -94,6 +98,8 @@
|
||||
7B6F97E6EF65AA17A469F037 /* ProcessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessView.swift; sourceTree = "<group>"; };
|
||||
813543165B39EBD213EB3BC7 /* AppsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsView.swift; sourceTree = "<group>"; };
|
||||
892247B0F17363DFC146EDF8 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||
8A1CB7CF2D7EA7A20E561192 /* WindowVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowVisibility.swift; sourceTree = "<group>"; };
|
||||
8E44036C6DB5C055C179A3E2 /* MenuBarStatusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarStatusTests.swift; sourceTree = "<group>"; };
|
||||
8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TopManager_icon.png; sourceTree = "<group>"; };
|
||||
8F402CE13EF67DE550786B79 /* MemoryDonutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDonutView.swift; sourceTree = "<group>"; };
|
||||
94D5A13CC5B9CF1B90CC2214 /* CPUChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUChartView.swift; sourceTree = "<group>"; };
|
||||
@@ -126,11 +132,13 @@
|
||||
162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */,
|
||||
BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */,
|
||||
27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */,
|
||||
5B37A9AE9F6215C0C132B8D9 /* MenuBarStatus.swift */,
|
||||
05CC33DC4E93F6ED2E52F4E1 /* MetricsStore.swift */,
|
||||
347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */,
|
||||
95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */,
|
||||
295232B4C746511757DCF8FE /* ProcessMonitor.swift */,
|
||||
279B972C08A878053821285D /* SystemMonitor.swift */,
|
||||
8A1CB7CF2D7EA7A20E561192 /* WindowVisibility.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
@@ -251,6 +259,7 @@
|
||||
70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */,
|
||||
301352EB5B362C732CE1A246 /* FormattingTests.swift */,
|
||||
E539A84E57291B69D4EE0314 /* HealthAlertTests.swift */,
|
||||
8E44036C6DB5C055C179A3E2 /* MenuBarStatusTests.swift */,
|
||||
0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */,
|
||||
332290DABD5326C87258723C /* ProcessDataTests.swift */,
|
||||
01C19ABF49697A97F0C40378 /* SettingsTests.swift */,
|
||||
@@ -374,6 +383,7 @@
|
||||
2DE14B65EA1D4E9B4D9DA5A6 /* BatteryMathTests.swift in Sources */,
|
||||
49D85FF611927294159C89D1 /* FormattingTests.swift in Sources */,
|
||||
489E29F78A4093D3548673E6 /* HealthAlertTests.swift in Sources */,
|
||||
DA03FF0D25DB8B9D3B09BC82 /* MenuBarStatusTests.swift in Sources */,
|
||||
24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */,
|
||||
2B74C177F49B4358D9642712 /* ProcessDataTests.swift in Sources */,
|
||||
8789C5F4A1AF4B1E8E434D71 /* SettingsTests.swift in Sources */,
|
||||
@@ -404,6 +414,7 @@
|
||||
7A1144E43159D96564C13164 /* MemoryInfo.swift in Sources */,
|
||||
266B888ADE2FDDBCEC8D28A5 /* MemoryLineChart.swift in Sources */,
|
||||
F5722D2B18EF7ABF43847EE4 /* MemoryMonitor.swift in Sources */,
|
||||
AE29356C1D41F33A3E94B6B7 /* MenuBarStatus.swift in Sources */,
|
||||
35852165B2DA76AB7F073723 /* MenuBarView.swift in Sources */,
|
||||
BCBAC37351B9A3878CC3F01B /* MetricsStore.swift in Sources */,
|
||||
CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */,
|
||||
@@ -421,6 +432,7 @@
|
||||
DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */,
|
||||
D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */,
|
||||
9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */,
|
||||
14D77C268F9F701E612E4FD3 /* WindowVisibility.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,9 @@ struct ProcessItem: Identifiable, Hashable {
|
||||
let user: String
|
||||
let cpuUsage: Double // Per-core: 100% = 1 core fully utilized
|
||||
let cpuUsageTotal: Double // Normalized: 100% = all cores fully utilized
|
||||
let memoryUsage: Int64
|
||||
let memoryUsage: Int64 // physical footprint (Activity Monitor's "Memory")
|
||||
let residentMemory: Int64 // pages currently in physical RAM, shared ones included
|
||||
let compressedMemory: Int64 // original size of pages in the compressor — in RAM or swapped out
|
||||
let threadCount: Int32
|
||||
let state: ProcessState
|
||||
let icon: NSImage?
|
||||
@@ -42,7 +44,9 @@ struct ProcessItem: Identifiable, Hashable {
|
||||
diskReadBytes: UInt64 = 0,
|
||||
diskWriteBytes: UInt64 = 0,
|
||||
energyImpact: Double = 0,
|
||||
executablePath: String? = nil
|
||||
executablePath: String? = nil,
|
||||
residentMemory: Int64 = 0,
|
||||
compressedMemory: Int64 = 0
|
||||
) {
|
||||
self.id = pid
|
||||
self.pid = pid
|
||||
@@ -51,6 +55,8 @@ struct ProcessItem: Identifiable, Hashable {
|
||||
self.cpuUsage = cpuUsage
|
||||
self.cpuUsageTotal = cpuUsageTotal
|
||||
self.memoryUsage = memoryUsage
|
||||
self.residentMemory = residentMemory
|
||||
self.compressedMemory = compressedMemory
|
||||
self.threadCount = threadCount
|
||||
self.state = state
|
||||
self.icon = icon
|
||||
@@ -73,6 +79,8 @@ struct ProcessItem: Identifiable, Hashable {
|
||||
lhs.cpuUsage == rhs.cpuUsage &&
|
||||
lhs.cpuUsageTotal == rhs.cpuUsageTotal &&
|
||||
lhs.memoryUsage == rhs.memoryUsage &&
|
||||
lhs.residentMemory == rhs.residentMemory &&
|
||||
lhs.compressedMemory == rhs.compressedMemory &&
|
||||
lhs.threadCount == rhs.threadCount &&
|
||||
lhs.state == rhs.state &&
|
||||
lhs.diskReadRate == rhs.diskReadRate &&
|
||||
|
||||
@@ -48,8 +48,12 @@ final class AlertCenter: ObservableObject {
|
||||
thermal: thermal,
|
||||
diskUsedFraction: diskFraction
|
||||
)
|
||||
healthScore = HealthModel.score(input)
|
||||
diagnosis = HealthModel.diagnosis(input)
|
||||
// Assign only on change: @Published fires on every set, and re-assigning
|
||||
// equal values each tick re-rendered every health view on every refresh.
|
||||
let score = HealthModel.score(input)
|
||||
if score != healthScore { healthScore = score }
|
||||
let findings = HealthModel.diagnosis(input)
|
||||
if findings != diagnosis { diagnosis = findings }
|
||||
|
||||
// CPU (sustained)
|
||||
let cpuEval = AlertEvaluator.sustained(breached: cpuUsage >= thresholds.cpuPercent,
|
||||
|
||||
30
TopManager/Services/MenuBarStatus.swift
Normal file
30
TopManager/Services/MenuBarStatus.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
/// The few always-live strings the menu-bar title shows.
|
||||
///
|
||||
/// Kept apart from `SystemMonitor` because the monitor stops publishing while no
|
||||
/// TopManager window is on screen (see `SystemMonitor.setUIVisible`), and the
|
||||
/// title must keep ticking regardless. Values are pre-formatted and only
|
||||
/// re-published when the visible text actually changes: `@Published` fires on
|
||||
/// every assignment, and each firing re-measures the status item.
|
||||
@MainActor
|
||||
final class MenuBarStatus: ObservableObject {
|
||||
static let shared = MenuBarStatus()
|
||||
|
||||
@Published private(set) var cpuText: String?
|
||||
@Published private(set) var memoryText: String?
|
||||
@Published private(set) var downloadText: String?
|
||||
|
||||
init() {}
|
||||
|
||||
func update(cpu: CPUInfo?, memory: MemoryInfo?, network: NetworkInfo?) {
|
||||
let cpu = cpu.map { String(format: "%.0f%%", $0.globalUsage) }
|
||||
if cpu != cpuText { cpuText = cpu }
|
||||
|
||||
let memory = memory.map { String(format: "%.0f%%", $0.usagePercentage) }
|
||||
if memory != memoryText { memoryText = memory }
|
||||
|
||||
let download = network.map { formatBytesPerSecond($0.totalDownloadRate) }
|
||||
if download != downloadText { downloadText = download }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ final class ProcessMonitor {
|
||||
private var previousCPUTimes: [pid_t: (user: UInt64, system: UInt64, timestamp: Date)] = [:]
|
||||
private var previousDiskIO: [pid_t: (read: UInt64, write: UInt64, idle: UInt64, timestamp: Date)] = [:]
|
||||
private var lastKnownCPU: [pid_t: Double] = [:] // Cache last known CPU usage
|
||||
private var lastDetailed: [pid_t: DetailedSample] = [:] // Reused on lightweight refreshes
|
||||
private let iconCache = NSCache<NSNumber, NSImage>()
|
||||
private var noIconPids: Set<pid_t> = [] // Cache for PIDs with no icon
|
||||
private var nameCache: [pid_t: String] = [:]
|
||||
@@ -56,6 +57,7 @@ final class ProcessMonitor {
|
||||
previousDiskIO.removeValue(forKey: pid)
|
||||
noIconPids.remove(pid)
|
||||
lastKnownCPU.removeValue(forKey: pid)
|
||||
lastDetailed.removeValue(forKey: pid)
|
||||
}
|
||||
|
||||
return processes
|
||||
@@ -81,14 +83,7 @@ final class ProcessMonitor {
|
||||
let lastCPU = lastKnownCPU[pid] ?? 0
|
||||
let needsDetailedInfo = fullRefresh || lastCPU > 0.1
|
||||
|
||||
let memoryUsage: Int64
|
||||
let threadCount: Int32
|
||||
let cpuUsage: Double
|
||||
var diskReadRate: Double = 0
|
||||
var diskWriteRate: Double = 0
|
||||
var diskReadBytes: UInt64 = 0
|
||||
var diskWriteBytes: UInt64 = 0
|
||||
var energyImpact: Double = 0
|
||||
let detail: DetailedSample
|
||||
|
||||
if needsDetailedInfo {
|
||||
var taskInfo = proc_taskinfo()
|
||||
@@ -98,9 +93,7 @@ final class ProcessMonitor {
|
||||
|
||||
if hasTaskInfo {
|
||||
let rusageData = fetchRusageData(pid: pid)
|
||||
memoryUsage = rusageData.memory
|
||||
threadCount = taskInfo.pti_threadnum
|
||||
cpuUsage = calculateCPUUsage(
|
||||
let cpu = calculateCPUUsage(
|
||||
pid: pid,
|
||||
userTime: rusageData.userTime,
|
||||
systemTime: rusageData.systemTime
|
||||
@@ -111,23 +104,33 @@ final class ProcessMonitor {
|
||||
diskWrite: rusageData.diskWrite,
|
||||
idleWakeups: rusageData.idleWakeups
|
||||
)
|
||||
diskReadRate = extra.readRate
|
||||
diskWriteRate = extra.writeRate
|
||||
diskReadBytes = rusageData.diskRead
|
||||
diskWriteBytes = rusageData.diskWrite
|
||||
energyImpact = EnergyModel.impact(cpuPercent: cpuUsage, idleWakeupsPerSec: extra.idleWakeupsPerSec)
|
||||
detail = DetailedSample(
|
||||
memory: rusageData.memory,
|
||||
resident: rusageData.resident,
|
||||
compressed: fetchCompressedMemory(pid: pid) ?? 0,
|
||||
threads: taskInfo.pti_threadnum,
|
||||
cpu: cpu,
|
||||
diskReadRate: extra.readRate,
|
||||
diskWriteRate: extra.writeRate,
|
||||
diskReadBytes: rusageData.diskRead,
|
||||
diskWriteBytes: rusageData.diskWrite,
|
||||
energy: EnergyModel.impact(cpuPercent: cpu, idleWakeupsPerSec: extra.idleWakeupsPerSec)
|
||||
)
|
||||
lastDetailed[pid] = detail
|
||||
} else {
|
||||
memoryUsage = 0
|
||||
threadCount = 0
|
||||
cpuUsage = 0
|
||||
detail = .zero
|
||||
}
|
||||
} else {
|
||||
// Lightweight refresh - reuse last known values
|
||||
memoryUsage = 0
|
||||
threadCount = 0
|
||||
cpuUsage = 0
|
||||
// Lightweight refresh - reuse last known values. This used to emit
|
||||
// zeros, so ~90% of rows showed "Zero KB" memory (and 0 threads/CPU)
|
||||
// on two of every three updates.
|
||||
detail = lastDetailed[pid] ?? .zero
|
||||
}
|
||||
|
||||
let memoryUsage = detail.memory
|
||||
let threadCount = detail.threads
|
||||
let cpuUsage = detail.cpu
|
||||
|
||||
let state = determineProcessState(status: bsdInfo.pbi_status, cpuUsage: cpuUsage)
|
||||
|
||||
let icon = fetchIcon(pid: pid)
|
||||
@@ -147,12 +150,14 @@ final class ProcessMonitor {
|
||||
icon: icon,
|
||||
parentPid: parentPid,
|
||||
startTime: startTime,
|
||||
diskReadRate: diskReadRate,
|
||||
diskWriteRate: diskWriteRate,
|
||||
diskReadBytes: diskReadBytes,
|
||||
diskWriteBytes: diskWriteBytes,
|
||||
energyImpact: energyImpact,
|
||||
executablePath: pathCache[pid]
|
||||
diskReadRate: detail.diskReadRate,
|
||||
diskWriteRate: detail.diskWriteRate,
|
||||
diskReadBytes: detail.diskReadBytes,
|
||||
diskWriteBytes: detail.diskWriteBytes,
|
||||
energyImpact: detail.energy,
|
||||
executablePath: pathCache[pid],
|
||||
residentMemory: detail.resident,
|
||||
compressedMemory: detail.compressed
|
||||
)
|
||||
}
|
||||
|
||||
@@ -230,8 +235,27 @@ final class ProcessMonitor {
|
||||
return name
|
||||
}
|
||||
|
||||
/// The expensive per-process readings (rusage + task info + derived rates).
|
||||
private struct DetailedSample {
|
||||
var memory: Int64
|
||||
var resident: Int64
|
||||
var compressed: Int64
|
||||
var threads: Int32
|
||||
var cpu: Double
|
||||
var diskReadRate: Double
|
||||
var diskWriteRate: Double
|
||||
var diskReadBytes: UInt64
|
||||
var diskWriteBytes: UInt64
|
||||
var energy: Double
|
||||
|
||||
static let zero = DetailedSample(memory: 0, resident: 0, compressed: 0, threads: 0, cpu: 0,
|
||||
diskReadRate: 0, diskWriteRate: 0,
|
||||
diskReadBytes: 0, diskWriteBytes: 0, energy: 0)
|
||||
}
|
||||
|
||||
private struct RusageData {
|
||||
var memory: Int64 = 0
|
||||
var resident: Int64 = 0
|
||||
var userTime: UInt64 = 0
|
||||
var systemTime: UInt64 = 0
|
||||
var diskRead: UInt64 = 0
|
||||
@@ -250,6 +274,7 @@ final class ProcessMonitor {
|
||||
if result == 0 {
|
||||
return RusageData(
|
||||
memory: Int64(rusage.ri_phys_footprint),
|
||||
resident: Int64(rusage.ri_resident_size),
|
||||
userTime: rusage.ri_user_time,
|
||||
systemTime: rusage.ri_system_time,
|
||||
diskRead: rusage.ri_diskio_bytesread,
|
||||
@@ -264,6 +289,7 @@ final class ProcessMonitor {
|
||||
if proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, taskInfoSize) == taskInfoSize {
|
||||
return RusageData(
|
||||
memory: Int64(taskInfo.pti_resident_size),
|
||||
resident: Int64(taskInfo.pti_resident_size),
|
||||
userTime: taskInfo.pti_total_user,
|
||||
systemTime: taskInfo.pti_total_system
|
||||
)
|
||||
@@ -475,6 +501,29 @@ func fetchProcessArguments(pid: pid_t) -> [String] {
|
||||
return ProcArgs.parse(Array(buffer.prefix(size)))?.args ?? []
|
||||
}
|
||||
|
||||
/// Memory of a process currently held by the compressor, at its original
|
||||
/// (uncompressed) size. macOS does not track swap per process: when compressor
|
||||
/// segments are paged out to disk they stay counted here, so this is the
|
||||
/// closest per-process "swapped" figure that exists — Activity Monitor's
|
||||
/// "Compressed Memory" and `top`'s CMPRS show the same value.
|
||||
///
|
||||
/// Needs only a task *name* port, which the kernel hands out for processes of
|
||||
/// the same user; returns `nil` for other users' and system processes.
|
||||
func fetchCompressedMemory(pid: pid_t) -> Int64? {
|
||||
var port: mach_port_name_t = 0
|
||||
guard task_name_for_pid(mach_task_self_, pid, &port) == KERN_SUCCESS else { return nil }
|
||||
defer { mach_port_deallocate(mach_task_self_, port) }
|
||||
|
||||
var info = task_vm_info_data_t()
|
||||
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.stride / MemoryLayout<natural_t>.stride)
|
||||
let result = withUnsafeMutablePointer(to: &info) {
|
||||
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
||||
task_info(port, task_flavor_t(TASK_VM_INFO), $0, &count)
|
||||
}
|
||||
}
|
||||
return result == KERN_SUCCESS ? Int64(info.compressed) : nil
|
||||
}
|
||||
|
||||
/// Counts open file descriptors for a process (files, sockets, pipes, …).
|
||||
func fetchOpenFileCount(pid: pid_t) -> Int? {
|
||||
let bufferSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
|
||||
|
||||
@@ -6,7 +6,8 @@ final class SystemMonitor: ObservableObject {
|
||||
|
||||
private let backgroundQueue = DispatchQueue(label: "com.topmanager.monitor", qos: .userInitiated)
|
||||
|
||||
// Published properties (must be updated on main thread)
|
||||
// Published properties (must be updated on main thread). They only follow
|
||||
// `live` while a TopManager window is on screen — see `setUIVisible`.
|
||||
@MainActor @Published var cpuInfo: CPUInfo?
|
||||
@MainActor @Published var memoryInfo: MemoryInfo?
|
||||
@MainActor @Published var processes: [ProcessItem] = []
|
||||
@@ -22,6 +23,24 @@ final class SystemMonitor: ObservableObject {
|
||||
@MainActor @Published var memoryHistory: [MemoryHistoryPoint] = []
|
||||
@MainActor @Published var networkHistory: [NetworkHistoryPoint] = []
|
||||
|
||||
/// Always-current readings and chart histories. Recording, alerts and the
|
||||
/// menu-bar title read from here, so they keep running while views are paused.
|
||||
private struct LiveState {
|
||||
var cpuInfo: CPUInfo?
|
||||
var memoryInfo: MemoryInfo?
|
||||
var processes: [ProcessItem] = []
|
||||
var diskInfo: DiskInfo?
|
||||
var networkInfo: NetworkInfo?
|
||||
var gpuInfo: GPUInfo?
|
||||
var powerInfo: PowerInfo?
|
||||
var cpuHistory: [CPUHistoryPoint] = []
|
||||
var coreHistories: [Int: [CoreHistoryPoint]] = [:]
|
||||
var memoryHistory: [MemoryHistoryPoint] = []
|
||||
var networkHistory: [NetworkHistoryPoint] = []
|
||||
}
|
||||
@MainActor private var live = LiveState()
|
||||
@MainActor private var isUIVisible = true
|
||||
|
||||
// Sub-monitors
|
||||
private let cpuMonitor = CPUMonitor()
|
||||
private let memoryMonitor = MemoryMonitor()
|
||||
@@ -47,6 +66,8 @@ final class SystemMonitor: ObservableObject {
|
||||
private init() {}
|
||||
|
||||
@MainActor func startMonitoring() {
|
||||
guard timer == nil else { return }
|
||||
|
||||
// Immediate initial fetch
|
||||
backgroundQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
@@ -134,95 +155,103 @@ final class SystemMonitor: ObservableObject {
|
||||
processes: [ProcessItem]?, disk: DiskInfo?, gpu: GPUInfo?,
|
||||
power: PowerInfo?) {
|
||||
if let info = cpu {
|
||||
cpuInfo = info
|
||||
let historyPoint = CPUHistoryPoint(
|
||||
live.cpuInfo = info
|
||||
live.cpuHistory.append(CPUHistoryPoint(
|
||||
timestamp: info.timestamp,
|
||||
usage: info.globalUsage,
|
||||
userUsage: info.userUsage,
|
||||
systemUsage: info.systemUsage
|
||||
)
|
||||
cpuHistory.append(historyPoint)
|
||||
if cpuHistory.count > historyLimit {
|
||||
cpuHistory.removeFirst()
|
||||
))
|
||||
if live.cpuHistory.count > historyLimit {
|
||||
live.cpuHistory.removeFirst()
|
||||
}
|
||||
|
||||
for core in info.coreUsages {
|
||||
let corePoint = CoreHistoryPoint(timestamp: info.timestamp, usage: core.usage)
|
||||
if coreHistories[core.id] == nil {
|
||||
coreHistories[core.id] = []
|
||||
}
|
||||
coreHistories[core.id]?.append(corePoint)
|
||||
if coreHistories[core.id]?.count ?? 0 > historyLimit {
|
||||
coreHistories[core.id]?.removeFirst()
|
||||
live.coreHistories[core.id, default: []].append(
|
||||
CoreHistoryPoint(timestamp: info.timestamp, usage: core.usage))
|
||||
if live.coreHistories[core.id, default: []].count > historyLimit {
|
||||
live.coreHistories[core.id]?.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let info = memory {
|
||||
memoryInfo = info
|
||||
let historyPoint = MemoryHistoryPoint(
|
||||
live.memoryInfo = info
|
||||
live.memoryHistory.append(MemoryHistoryPoint(
|
||||
timestamp: info.timestamp,
|
||||
usedMemory: info.usedMemory,
|
||||
totalMemory: info.totalMemory
|
||||
)
|
||||
memoryHistory.append(historyPoint)
|
||||
if memoryHistory.count > historyLimit {
|
||||
memoryHistory.removeFirst()
|
||||
))
|
||||
if live.memoryHistory.count > historyLimit {
|
||||
live.memoryHistory.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
if let info = network {
|
||||
networkInfo = info
|
||||
let historyPoint = NetworkHistoryPoint(
|
||||
live.networkInfo = info
|
||||
live.networkHistory.append(NetworkHistoryPoint(
|
||||
timestamp: info.timestamp,
|
||||
downloadRate: info.totalDownloadRate,
|
||||
uploadRate: info.totalUploadRate
|
||||
)
|
||||
networkHistory.append(historyPoint)
|
||||
if networkHistory.count > historyLimit {
|
||||
networkHistory.removeFirst()
|
||||
))
|
||||
if live.networkHistory.count > historyLimit {
|
||||
live.networkHistory.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
if let procs = processes {
|
||||
self.processes = procs
|
||||
}
|
||||
if let procs = processes { live.processes = procs }
|
||||
if let info = disk { live.diskInfo = info }
|
||||
if let info = gpu { live.gpuInfo = info }
|
||||
if let info = power { live.powerInfo = info }
|
||||
|
||||
if let info = disk {
|
||||
diskInfo = info
|
||||
}
|
||||
|
||||
if let info = gpu {
|
||||
gpuInfo = info
|
||||
}
|
||||
|
||||
if let info = power {
|
||||
powerInfo = info
|
||||
}
|
||||
if isUIVisible { publishLive() }
|
||||
MenuBarStatus.shared.update(cpu: live.cpuInfo, memory: live.memoryInfo, network: live.networkInfo)
|
||||
|
||||
// Persist a system-wide sample so history survives restarts and long ranges.
|
||||
if let c = cpuInfo, let m = memoryInfo {
|
||||
if let c = live.cpuInfo, let m = live.memoryInfo {
|
||||
MetricsStore.shared.record(MetricsSample(
|
||||
t: c.timestamp,
|
||||
cpu: c.globalUsage,
|
||||
memUsed: m.usedMemory,
|
||||
memTotal: m.totalMemory,
|
||||
netDown: networkInfo?.totalDownloadRate ?? 0,
|
||||
netUp: networkInfo?.totalUploadRate ?? 0
|
||||
netDown: live.networkInfo?.totalDownloadRate ?? 0,
|
||||
netUp: live.networkInfo?.totalUploadRate ?? 0
|
||||
))
|
||||
}
|
||||
|
||||
// Proactive health + alert evaluation (runs every cycle).
|
||||
AlertCenter.shared.evaluate(
|
||||
cpuUsage: cpuInfo?.globalUsage ?? 0,
|
||||
memory: memoryInfo,
|
||||
disk: diskInfo,
|
||||
cpuUsage: live.cpuInfo?.globalUsage ?? 0,
|
||||
memory: live.memoryInfo,
|
||||
disk: live.diskInfo,
|
||||
thermal: currentThermalLevel(),
|
||||
topProcess: self.processes.max { $0.cpuUsage < $1.cpuUsage },
|
||||
power: powerInfo
|
||||
topProcess: live.processes.max { $0.cpuUsage < $1.cpuUsage },
|
||||
power: live.powerInfo
|
||||
)
|
||||
}
|
||||
|
||||
/// Pause (`false`) or resume (`true`) publishing to views. Resuming catches
|
||||
/// the views up at once with everything recorded while they were hidden.
|
||||
@MainActor func setUIVisible(_ visible: Bool) {
|
||||
guard visible != isUIVisible else { return }
|
||||
isUIVisible = visible
|
||||
if visible { publishLive() }
|
||||
}
|
||||
|
||||
@MainActor private func publishLive() {
|
||||
cpuInfo = live.cpuInfo
|
||||
memoryInfo = live.memoryInfo
|
||||
processes = live.processes
|
||||
diskInfo = live.diskInfo
|
||||
networkInfo = live.networkInfo
|
||||
gpuInfo = live.gpuInfo
|
||||
powerInfo = live.powerInfo
|
||||
cpuHistory = live.cpuHistory
|
||||
coreHistories = live.coreHistories
|
||||
memoryHistory = live.memoryHistory
|
||||
networkHistory = live.networkHistory
|
||||
}
|
||||
|
||||
private func currentThermalLevel() -> ThermalLevel {
|
||||
switch ProcessInfo.processInfo.thermalState {
|
||||
case .nominal: return .nominal
|
||||
|
||||
52
TopManager/Services/WindowVisibility.swift
Normal file
52
TopManager/Services/WindowVisibility.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import AppKit
|
||||
|
||||
/// Reports whether any TopManager window is actually on screen: the main window,
|
||||
/// the menu-bar popover or Settings — as opposed to closed, minimized, fully
|
||||
/// covered, or on another Space.
|
||||
///
|
||||
/// Why this exists: on macOS 26 several framework paths leak a little on every
|
||||
/// view update (SwiftUI `Table` cell updates, AppKit text-cell KVO dependencies,
|
||||
/// an Observation registrar created inside DesignLibrary). SwiftUI keeps updating
|
||||
/// a minimized or covered window, so leaving the main window open all day grew
|
||||
/// the app by several hundred MB. `SystemMonitor` stops publishing while this
|
||||
/// reports `false`, which confines that growth to the time someone is looking.
|
||||
@MainActor
|
||||
final class WindowVisibility {
|
||||
static let shared = WindowVisibility()
|
||||
|
||||
private(set) var isAnyWindowVisible = true
|
||||
private var onChange: ((Bool) -> Void)?
|
||||
private var tokens: [NSObjectProtocol] = []
|
||||
|
||||
private init() {}
|
||||
|
||||
func start(onChange: @escaping (Bool) -> Void) {
|
||||
guard tokens.isEmpty else { return }
|
||||
self.onChange = onChange
|
||||
|
||||
let names = [NSWindow.didChangeOcclusionStateNotification, NSWindow.willCloseNotification]
|
||||
for name in names {
|
||||
tokens.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
||||
// willClose arrives while the window is still on screen, so
|
||||
// re-check on a later turn, once it is actually gone.
|
||||
Task { @MainActor in self?.recompute() }
|
||||
})
|
||||
}
|
||||
recompute()
|
||||
}
|
||||
|
||||
private func recompute() {
|
||||
let visible = NSApp.windows.contains(where: Self.countsAsVisible)
|
||||
guard visible != isAnyWindowVisible else { return }
|
||||
isAnyWindowVisible = visible
|
||||
onChange?(visible)
|
||||
}
|
||||
|
||||
/// The status item's host window is permanently on screen; counting it would
|
||||
/// keep the UI publishing forever.
|
||||
private static func countsAsVisible(_ window: NSWindow) -> Bool {
|
||||
window.isVisible
|
||||
&& window.occlusionState.contains(.visible)
|
||||
&& !String(describing: type(of: window)).contains("StatusBarWindow")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,16 @@ import UserNotifications
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
// Start here rather than in the main window's onAppear: the app can launch
|
||||
// with no window at all (state restoration after the window was closed),
|
||||
// and the menu bar, alerts and history must run regardless.
|
||||
Task { @MainActor in
|
||||
SystemMonitor.shared.startMonitoring()
|
||||
AppSettings.shared.applyAll()
|
||||
AlertCenter.shared.requestNotificationAuthorization()
|
||||
WindowVisibility.shared.start { SystemMonitor.shared.setUIVisible($0) }
|
||||
}
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
@@ -33,11 +43,6 @@ struct TopManagerApp: App {
|
||||
.environmentObject(monitor)
|
||||
.environmentObject(AlertCenter.shared)
|
||||
.environmentObject(settings)
|
||||
.onAppear {
|
||||
monitor.startMonitoring()
|
||||
settings.applyAll()
|
||||
AlertCenter.shared.requestNotificationAuthorization()
|
||||
}
|
||||
}
|
||||
.windowStyle(.automatic)
|
||||
.defaultSize(width: 1000, height: 700)
|
||||
@@ -62,7 +67,7 @@ struct TopManagerApp: App {
|
||||
.environmentObject(AlertCenter.shared)
|
||||
.environmentObject(settings)
|
||||
} label: {
|
||||
MenuBarLabel(monitor: monitor, settings: settings, alerts: AlertCenter.shared)
|
||||
MenuBarLabel(status: MenuBarStatus.shared, settings: settings, alerts: AlertCenter.shared)
|
||||
}
|
||||
.menuBarExtraStyle(.window)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The compact menu-bar title. Renders whichever metric the user picked.
|
||||
/// Reads `MenuBarStatus`, not `SystemMonitor`, so it keeps updating while the
|
||||
/// monitor's view publishing is paused.
|
||||
struct MenuBarLabel: View {
|
||||
@ObservedObject var monitor: SystemMonitor
|
||||
@ObservedObject var status: MenuBarStatus
|
||||
@ObservedObject var settings: AppSettings
|
||||
@ObservedObject var alerts: AlertCenter
|
||||
|
||||
var body: some View {
|
||||
switch settings.menuBarMetric {
|
||||
case .cpu:
|
||||
textOrIcon(monitor.cpuInfo.map { String(format: "%.0f%%", $0.globalUsage) }, icon: "cpu")
|
||||
textOrIcon(status.cpuText, icon: "cpu")
|
||||
case .memory:
|
||||
textOrIcon(monitor.memoryInfo.map { String(format: "%.0f%%", $0.usagePercentage) }, icon: "memorychip")
|
||||
textOrIcon(status.memoryText, icon: "memorychip")
|
||||
case .health:
|
||||
Text("♥ \(alerts.healthScore)").monospacedDigit()
|
||||
case .download:
|
||||
textOrIcon(monitor.networkInfo.map { formatBytesPerSecond($0.totalDownloadRate) }, icon: "arrow.down")
|
||||
textOrIcon(status.downloadText, icon: "arrow.down")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,8 @@ struct ProcessInspectorSheet: View {
|
||||
DetailRow(label: "CPU (total)", value: String(format: "%.2f%%", process.cpuUsageTotal))
|
||||
DetailRow(label: "Energy", value: String(format: "%.1f", process.energyImpact))
|
||||
DetailRow(label: "Memory", value: formatBytes(process.memoryUsage))
|
||||
DetailRow(label: "RAM", value: formatBytes(process.residentMemory))
|
||||
DetailRow(label: "Compressed", value: process.compressedMemory > 0 ? formatBytes(process.compressedMemory) : "—")
|
||||
DetailRow(label: "Threads", value: "\(process.threadCount)")
|
||||
DetailRow(label: "Open files", value: openFiles.map(String.init) ?? "…")
|
||||
DetailRow(label: "Disk read/s", value: process.diskReadRate > 0 ? formatBytesPerSecond(process.diskReadRate) : "—")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
enum ProcessSortColumn: String {
|
||||
case name, pid, cpu, cpuTotal, memory, threads, user, state, energy, disk
|
||||
case name, pid, cpu, cpuTotal, memory, resident, compressed, threads, user, state, energy, disk
|
||||
}
|
||||
|
||||
struct ProcessView: View {
|
||||
@@ -60,6 +60,10 @@ struct ProcessView: View {
|
||||
comparison = lhs.cpuUsageTotal < rhs.cpuUsageTotal ? .orderedAscending : (lhs.cpuUsageTotal > rhs.cpuUsageTotal ? .orderedDescending : .orderedSame)
|
||||
case .memory:
|
||||
comparison = lhs.memoryUsage < rhs.memoryUsage ? .orderedAscending : (lhs.memoryUsage > rhs.memoryUsage ? .orderedDescending : .orderedSame)
|
||||
case .resident:
|
||||
comparison = lhs.residentMemory < rhs.residentMemory ? .orderedAscending : (lhs.residentMemory > rhs.residentMemory ? .orderedDescending : .orderedSame)
|
||||
case .compressed:
|
||||
comparison = lhs.compressedMemory < rhs.compressedMemory ? .orderedAscending : (lhs.compressedMemory > rhs.compressedMemory ? .orderedDescending : .orderedSame)
|
||||
case .threads:
|
||||
comparison = lhs.threadCount < rhs.threadCount ? .orderedAscending : (lhs.threadCount > rhs.threadCount ? .orderedDescending : .orderedSame)
|
||||
case .user:
|
||||
@@ -87,6 +91,34 @@ struct ProcessView: View {
|
||||
return monitor.processes.first { $0.pid == pid }
|
||||
}
|
||||
|
||||
/// Memory, RAM and Compressed as a single builder element: Table accepts at
|
||||
/// most 10 columns per block, and inlining them also exceeds the type
|
||||
/// checker's time limit for the table expression.
|
||||
@TableColumnBuilder<ProcessItem, KeyPathComparator<ProcessItem>>
|
||||
private var memoryColumns: some TableColumnContent<ProcessItem, KeyPathComparator<ProcessItem>> {
|
||||
TableColumn("Memory", value: \.memoryUsage) { process in
|
||||
Text(formatBytes(process.memoryUsage))
|
||||
.monospacedDigit()
|
||||
}
|
||||
.width(80)
|
||||
|
||||
TableColumn("RAM", value: \.residentMemory) { process in
|
||||
Text(formatBytes(process.residentMemory))
|
||||
.monospacedDigit()
|
||||
}
|
||||
.width(80)
|
||||
|
||||
TableColumn("Compressed", value: \.compressedMemory) { process in
|
||||
if process.compressedMemory > 0 {
|
||||
Text(formatBytes(process.compressedMemory))
|
||||
.monospacedDigit()
|
||||
} else {
|
||||
Text("—").foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.width(85)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Summary bar - separate view to avoid re-rendering table
|
||||
@@ -140,11 +172,7 @@ struct ProcessView: View {
|
||||
}
|
||||
.width(60)
|
||||
|
||||
TableColumn("Memory", value: \.memoryUsage) { process in
|
||||
Text(formatBytes(process.memoryUsage))
|
||||
.monospacedDigit()
|
||||
}
|
||||
.width(80)
|
||||
memoryColumns
|
||||
|
||||
TableColumn("Disk I/O", value: \.diskTotalRate) { process in
|
||||
if process.diskTotalRate > 0 {
|
||||
@@ -246,6 +274,10 @@ struct ProcessView: View {
|
||||
sortColumn = .energy
|
||||
} else if keyPathString.contains("diskTotalRate") {
|
||||
sortColumn = .disk
|
||||
} else if keyPathString.contains("residentMemory") {
|
||||
sortColumn = .resident
|
||||
} else if keyPathString.contains("compressedMemory") {
|
||||
sortColumn = .compressed
|
||||
} else if keyPathString.contains("memoryUsage") {
|
||||
sortColumn = .memory
|
||||
} else if keyPathString.contains("name") {
|
||||
|
||||
35
TopManagerTests/MenuBarStatusTests.swift
Normal file
35
TopManagerTests/MenuBarStatusTests.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
import XCTest
|
||||
@testable import TopManager
|
||||
|
||||
final class MenuBarStatusTests: XCTestCase {
|
||||
|
||||
private func cpu(_ usage: Double) -> CPUInfo {
|
||||
CPUInfo(globalUsage: usage, userUsage: usage, systemUsage: 0, idleUsage: 100 - usage, coreUsages: [])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testFormatsVisibleText() {
|
||||
let status = MenuBarStatus()
|
||||
status.update(cpu: cpu(41.6), memory: nil, network: nil)
|
||||
XCTAssertEqual(status.cpuText, "42%")
|
||||
XCTAssertNil(status.memoryText)
|
||||
XCTAssertNil(status.downloadText)
|
||||
}
|
||||
|
||||
/// Every publish re-measures the status item, so a refresh that renders the
|
||||
/// same text must not publish at all.
|
||||
@MainActor
|
||||
func testRepublishesOnlyWhenVisibleTextChanges() {
|
||||
let status = MenuBarStatus()
|
||||
var publishes = 0
|
||||
let subscription = status.objectWillChange.sink { publishes += 1 }
|
||||
defer { subscription.cancel() }
|
||||
|
||||
status.update(cpu: cpu(41.6), memory: nil, network: nil)
|
||||
XCTAssertEqual(publishes, 1)
|
||||
status.update(cpu: cpu(42.4), memory: nil, network: nil) // still "42%"
|
||||
XCTAssertEqual(publishes, 1)
|
||||
status.update(cpu: cpu(57), memory: nil, network: nil)
|
||||
XCTAssertEqual(publishes, 2)
|
||||
}
|
||||
}
|
||||
@@ -92,4 +92,37 @@ final class ProcessDataTests: XCTestCase {
|
||||
XCTAssertNotEqual(make(energy: 5), make(energy: 9))
|
||||
XCTAssertEqual(make(energy: 5), make(energy: 5))
|
||||
}
|
||||
|
||||
// MARK: - Live sampling
|
||||
|
||||
/// Fetches 1–3 are full refreshes; 4 and 5 are lightweight ones, which used
|
||||
/// to report 0 memory for every idle process (~90% of rows). Only processes
|
||||
/// spawned between fetches may legitimately have no reading yet.
|
||||
func testLightweightRefreshKeepsLastKnownMemory() {
|
||||
let monitor = ProcessMonitor()
|
||||
let me = NSUserName()
|
||||
for fetch in 1...6 {
|
||||
let own = monitor.fetchProcesses().filter { $0.user == me }
|
||||
XCTAssertFalse(own.isEmpty)
|
||||
let zero = own.filter { $0.memoryUsage == 0 }.count
|
||||
XCTAssertLessThan(zero, max(3, own.count / 10),
|
||||
"fetch \(fetch): \(zero) of \(own.count) own processes reported 0 memory")
|
||||
let zeroRAM = own.filter { $0.residentMemory == 0 }.count
|
||||
XCTAssertLessThan(zeroRAM, max(3, own.count / 10),
|
||||
"fetch \(fetch): \(zeroRAM) of \(own.count) own processes reported 0 RAM")
|
||||
}
|
||||
}
|
||||
|
||||
func testOwnProcessReportsResidentMemory() {
|
||||
let item = ProcessMonitor().fetchProcesses().first { $0.pid == getpid() }
|
||||
XCTAssertNotNil(item)
|
||||
XCTAssertGreaterThan(item?.residentMemory ?? 0, 0)
|
||||
XCTAssertGreaterThan(item?.memoryUsage ?? 0, 0)
|
||||
}
|
||||
|
||||
/// A task name port is granted for our own processes only; launchd runs as root.
|
||||
func testCompressedMemoryReadableForOwnProcessesOnly() {
|
||||
XCTAssertNotNil(fetchCompressedMemory(pid: getpid()))
|
||||
XCTAssertNil(fetchCompressedMemory(pid: 1))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user