From e434e95fe582ead8259d3cf205315b785971ae71 Mon Sep 17 00:00:00 2001 From: Ivan Karpan Date: Tue, 11 Nov 2025 14:37:26 +0000 Subject: [PATCH 1/3] fix(2785): performance cores showing 75% instead of 100% when fully utilized replace CPU ID-based matching with positional matching in core usage calculations extract calculation logic into testable public functions (calculatePerformanceCoresUsage, calculateEfficiencyCoresUsage) add array count validation before performing calculations add comprehensive test suite (13 tests) covering bug scenario, edge cases, and positional matching update SwiftLint config: rename redundant_optional_initialization to implicit_optional_initialization remove unnecessary SwiftLint disable comment in Charts.swift fix RAM test: update test case PID to avoid conflicts Fixes #2785 --- .swiftlint.yml | 2 +- Kit/plugins/Charts.swift | 1 - Modules/CPU/readers.swift | 48 +++++-- Stats.xcodeproj/project.pbxproj | 4 + Tests/CPU.swift | 243 ++++++++++++++++++++++++++++++++ Tests/RAM.swift | 4 +- 6 files changed, 283 insertions(+), 19 deletions(-) create mode 100644 Tests/CPU.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index afd09fbc931..ce3a13e0dc3 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,7 +5,7 @@ disabled_rules: - trailing_whitespace - opening_brace - implicit_getter - - redundant_optional_initialization + - implicit_optional_initialization - large_tuple - function_body_length diff --git a/Kit/plugins/Charts.swift b/Kit/plugins/Charts.swift index 1ee0de3b1ae..c0dfa782967 100644 --- a/Kit/plugins/Charts.swift +++ b/Kit/plugins/Charts.swift @@ -852,7 +852,6 @@ public class BarChartView: NSView { let blocks: Int = 16 let spacing: CGFloat = 2 let count: CGFloat = CGFloat(values.count) - // swiftlint:disable:next empty_count guard count > 0, self.frame.width > 0, self.frame.height > 0 else { return } let partitionSize: CGSize = CGSize(width: (self.frame.width - (count*spacing)) / count, height: self.frame.height) diff --git a/Modules/CPU/readers.swift b/Modules/CPU/readers.swift index e822fbcab21..352a8f424c5 100644 --- a/Modules/CPU/readers.swift +++ b/Modules/CPU/readers.swift @@ -12,6 +12,35 @@ import Cocoa import Kit +/// Calculate performance cores usage from cores and usagePerCore arrays +/// Uses positional matching (cores[i] -> usagePerCore[i]) to handle cases where CPU IDs don't match sequential indices +/// This fixes issue #2785 +public func calculatePerformanceCoresUsage(cores: [core_s], usagePerCore: [Double]) -> Double? { + guard cores.count == usagePerCore.count else { return nil } + let pCoresList: [Double] = cores.enumerated().compactMap { (index, core) -> Double? in + guard core.type == .performance, index < usagePerCore.count else { + return nil + } + return usagePerCore[index] + } + guard !pCoresList.isEmpty else { return nil } + return pCoresList.reduce(0, +) / Double(pCoresList.count) +} + +/// Calculate efficiency cores usage from cores and usagePerCore arrays +/// Uses positional matching (cores[i] -> usagePerCore[i]) to handle cases where CPU IDs don't match sequential indices +public func calculateEfficiencyCoresUsage(cores: [core_s], usagePerCore: [Double]) -> Double? { + guard cores.count == usagePerCore.count else { return nil } + let eCoresList: [Double] = cores.enumerated().compactMap { (index, core) -> Double? in + guard core.type == .efficiency, index < usagePerCore.count else { + return nil + } + return usagePerCore[index] + } + guard !eCoresList.isEmpty else { return nil } + return eCoresList.reduce(0, +) / Double(eCoresList.count) +} + internal class LoadReader: Reader { private var cpuInfo: processor_info_array_t! private var prevCpuInfo: processor_info_array_t? @@ -130,21 +159,10 @@ internal class LoadReader: Reader { self.response.totalUsage = self.response.systemLoad + self.response.userLoad if let cores = self.cores { - let eCoresList: [Double] = cores.filter({ $0.type == .efficiency }).compactMap { (c: core_s) in - if self.response.usagePerCore.indices.contains(Int(c.id)) { - return self.response.usagePerCore[Int(c.id)] - } - return 0 - } - let pCoresList: [Double] = cores.filter({ $0.type == .performance }).compactMap { (c: core_s) in - if self.response.usagePerCore.indices.contains(Int(c.id)) { - return self.response.usagePerCore[Int(c.id)] - } - return 0 - } - - self.response.usageECores = eCoresList.reduce(0, +)/Double(eCoresList.count) - self.response.usagePCores = pCoresList.reduce(0, +)/Double(pCoresList.count) + // Use positional matching instead of CPU ID to fix issue #2785 + // Match cores to usagePerCore by position in the array, not by CPU ID + self.response.usageECores = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: self.response.usagePerCore) + self.response.usagePCores = calculatePerformanceCoresUsage(cores: cores, usagePerCore: self.response.usagePerCore) } self.callback(self.response) diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index 802cb004011..7a98d014ed6 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 11426C9F2EC374BC00EFF024 /* CPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11426C9E2EC374BC00EFF024 /* CPU.swift */; }; 5C038CF62D86EE8A00516809 /* Remote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C038CF52D86EE8700516809 /* Remote.swift */; }; 5C044F7A2B3DE6F3005F6951 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C044F792B3DE6F3005F6951 /* portal.swift */; }; 5C0A2A8A292A5B4D009B4C1F /* SMJobBlessUtil.py in Resources */ = {isa = PBXBuildFile; fileRef = 5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */; }; @@ -489,6 +490,7 @@ /* Begin PBXFileReference section */ 0192A7ED2B017E190056F918 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; }; + 11426C9E2EC374BC00EFF024 /* CPU.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPU.swift; sourceTree = ""; }; 13BE56042D0E6963001C58EC /* en-AU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-AU"; path = "en-AU.lproj/Localizable.strings"; sourceTree = ""; }; 2658929724FC0F3B00FB3B6A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 29AA17CA2926879000709C01 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -1212,6 +1214,7 @@ 9AAC5E2B280ACC120043D892 /* Tests */ = { isa = PBXGroup; children = ( + 11426C9E2EC374BC00EFF024 /* CPU.swift */, 9AAC5E2E280ACC120043D892 /* Info.plist */, 9AAC5E40280ACC210043D892 /* RAM.swift */, ); @@ -2189,6 +2192,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 11426C9F2EC374BC00EFF024 /* CPU.swift in Sources */, 9AAC5E41280ACC210043D892 /* RAM.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/CPU.swift b/Tests/CPU.swift new file mode 100644 index 00000000000..99b4861011d --- /dev/null +++ b/Tests/CPU.swift @@ -0,0 +1,243 @@ +// +// CPU.swift +// Tests +// +// Created to test CPU performance cores utilization calculation fix. +// Testing issue #2785: Performance cores showing 75% instead of 100% when fully utilized. +// +// Copyright © 2025. All rights reserved. +// + +import XCTest +import CPU +import Kit + +class CPU: XCTestCase { + + /// Helper function to create core_s instances using JSON decoding + private func makeCore(id: Int32, type: coreType) -> core_s { + let json = """ + {"id": \(id), "type": \(type.rawValue)} + """ + guard let data = json.data(using: .utf8), + let core = try? JSONDecoder().decode(core_s.self, from: data) else { + fatalError("Failed to create core_s instance") + } + return core + } + + /// Test that reproduces issue #2785: Performance cores showing 75% instead of 100% + /// This test demonstrates the bug scenario where CPU IDs don't match sequential indices + /// The test EXPECTS 100% (correct behavior) and will FAIL with buggy code showing 75% + func testPerformanceCoresShouldShow100Percent_Issue2785() throws { + // Simulate M4 Pro 12/16: 4 efficiency + 8 performance cores = 12 total + // usagePerCore is built sequentially from host_processor_info (indices 0-11) + // All performance cores (indices 4-11) are at 100% utilization + let usagePerCore: [Double] = [ + 0.5, 0.5, 0.5, 0.5, // indices 0-3: efficiency cores + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 // indices 4-11: performance cores (ALL at 100%) + ] + + // BUG SCENARIO: Cores array has performance cores with IDs that don't all match sequential indices + // Some performance cores have IDs 4-9 (valid), but 2 have IDs 20,21 (out of bounds) + // This simulates what might happen when IORegistry returns CPU IDs that don't align + // with the sequential logical CPU indices from host_processor_info + let cores: [core_s] = [ + makeCore(id: 0, type: coreType.efficiency), + makeCore(id: 1, type: coreType.efficiency), + makeCore(id: 2, type: coreType.efficiency), + makeCore(id: 3, type: coreType.efficiency), + makeCore(id: 4, type: coreType.performance), + makeCore(id: 5, type: coreType.performance), + makeCore(id: 6, type: coreType.performance), + makeCore(id: 7, type: coreType.performance), + makeCore(id: 8, type: coreType.performance), + makeCore(id: 9, type: coreType.performance), + makeCore(id: 20, type: coreType.performance), // CPU ID 20 is out of bounds for usagePerCore[0-11] + makeCore(id: 21, type: coreType.performance) // CPU ID 21 is out of bounds for usagePerCore[0-11] + ] + + // Test the actual implementation function from LoadReader + // This tests the fix for issue #2785: positional matching instead of CPU ID matching + let usagePCores = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) + + // EXPECTATION: Should show 100% when all performance cores are at 100% + // With buggy ID-based code, this would show 75% (6 cores at 100%, 2 at 0%) instead of 100% + XCTAssertNotNil(usagePCores, "Performance cores usage should be calculated") + XCTAssertEqual(usagePCores!, 1.0, accuracy: 0.01, + "When all 8 performance cores are at 100%, should show 100%, not 75%") + XCTAssertEqual(cores.filter({ $0.type == .performance }).count, 8, "Should have 8 performance cores") + } + + // MARK: - calculatePerformanceCoresUsage Tests + + func testCalculatePerformanceCoresUsage_AllCoresAt100Percent() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 1, type: .performance), + makeCore(id: 2, type: .performance), + makeCore(id: 3, type: .performance) + ] + let usagePerCore: [Double] = [0.5, 1.0, 1.0, 1.0] + + let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNotNil(result) + XCTAssertEqual(result!, 1.0, accuracy: 0.01, "All performance cores at 100% should return 100%") + } + + func testCalculatePerformanceCoresUsage_MixedUsage() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 1, type: .performance), + makeCore(id: 2, type: .performance), + makeCore(id: 3, type: .performance) + ] + let usagePerCore: [Double] = [0.5, 0.5, 0.75, 1.0] + + let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNotNil(result) + let expected = (0.5 + 0.75 + 1.0) / 3.0 + XCTAssertEqual(result!, expected, accuracy: 0.01, "Should calculate average of performance cores") + } + + func testCalculatePerformanceCoresUsage_NoPerformanceCores() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 1, type: .efficiency), + makeCore(id: 2, type: .efficiency) + ] + let usagePerCore: [Double] = [0.5, 0.6, 0.7] + + let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNil(result, "Should return nil when there are no performance cores") + } + + func testCalculatePerformanceCoresUsage_CountMismatch() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .performance), + makeCore(id: 1, type: .performance) + ] + let usagePerCore: [Double] = [1.0, 1.0, 1.0] // 3 elements, but only 2 cores + + let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNil(result, "Should return nil when counts don't match") + } + + func testCalculatePerformanceCoresUsage_EmptyArrays() throws { + let cores: [core_s] = [] + let usagePerCore: [Double] = [] + + let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNil(result, "Should return nil for empty arrays") + } + + func testCalculatePerformanceCoresUsage_PositionalMatching() throws { + // Test that positional matching works correctly even with non-sequential IDs + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 5, type: .performance), // ID 5, but position 1 + makeCore(id: 10, type: .performance), // ID 10, but position 2 + makeCore(id: 20, type: .performance) // ID 20, but position 3 + ] + let usagePerCore: [Double] = [0.3, 0.8, 0.9, 1.0] // Position-based, not ID-based + + let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNotNil(result) + let expected = (0.8 + 0.9 + 1.0) / 3.0 + XCTAssertEqual(result!, expected, accuracy: 0.01, "Should use positional matching, not ID-based") + } + + // MARK: - calculateEfficiencyCoresUsage Tests + + func testCalculateEfficiencyCoresUsage_AllCoresAt50Percent() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 1, type: .efficiency), + makeCore(id: 2, type: .performance), + makeCore(id: 3, type: .performance) + ] + let usagePerCore: [Double] = [0.5, 0.5, 1.0, 1.0] + + let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNotNil(result) + XCTAssertEqual(result!, 0.5, accuracy: 0.01, "All efficiency cores at 50% should return 50%") + } + + func testCalculateEfficiencyCoresUsage_MixedUsage() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 1, type: .efficiency), + makeCore(id: 2, type: .efficiency), + makeCore(id: 3, type: .performance) + ] + let usagePerCore: [Double] = [0.3, 0.5, 0.7, 1.0] + + let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNotNil(result) + let expected = (0.3 + 0.5 + 0.7) / 3.0 + XCTAssertEqual(result!, expected, accuracy: 0.01, "Should calculate average of efficiency cores") + } + + func testCalculateEfficiencyCoresUsage_NoEfficiencyCores() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .performance), + makeCore(id: 1, type: .performance), + makeCore(id: 2, type: .performance) + ] + let usagePerCore: [Double] = [1.0, 1.0, 1.0] + + let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNil(result, "Should return nil when there are no efficiency cores") + } + + func testCalculateEfficiencyCoresUsage_CountMismatch() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 1, type: .efficiency) + ] + let usagePerCore: [Double] = [0.5, 0.6, 0.7] // 3 elements, but only 2 cores + + let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNil(result, "Should return nil when counts don't match") + } + + func testCalculateEfficiencyCoresUsage_PositionalMatching() throws { + // Test that positional matching works correctly even with non-sequential IDs + let cores: [core_s] = [ + makeCore(id: 100, type: .efficiency), // ID 100, but position 0 + makeCore(id: 200, type: .efficiency), // ID 200, but position 1 + makeCore(id: 300, type: .performance) + ] + let usagePerCore: [Double] = [0.4, 0.6, 1.0] // Position-based, not ID-based + + let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNotNil(result) + let expected = (0.4 + 0.6) / 2.0 + XCTAssertEqual(result!, expected, accuracy: 0.01, "Should use positional matching, not ID-based") + } + + func testCalculateEfficiencyCoresUsage_ZeroUsage() throws { + let cores: [core_s] = [ + makeCore(id: 0, type: .efficiency), + makeCore(id: 1, type: .efficiency), + makeCore(id: 2, type: .performance) + ] + let usagePerCore: [Double] = [0.0, 0.0, 1.0] + + let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) + + XCTAssertNotNil(result) + XCTAssertEqual(result!, 0.0, accuracy: 0.01, "Should handle zero usage correctly") + } +} diff --git a/Tests/RAM.swift b/Tests/RAM.swift index 2a9b1b83942..b827b0bfee1 100644 --- a/Tests/RAM.swift +++ b/Tests/RAM.swift @@ -34,8 +34,8 @@ class RAM: XCTestCase { XCTAssertEqual(process.name, "NotificationCent") XCTAssertEqual(process.usage, 62 * Double(1000 * 1000)) - process = ProcessReader.parseProcess("623 SafariCloudHisto 1608K") - XCTAssertEqual(process.pid, 623) + process = ProcessReader.parseProcess("99999 SafariCloudHisto 1608K") + XCTAssertEqual(process.pid, 99999) XCTAssertEqual(process.name, "SafariCloudHisto") XCTAssertEqual(process.usage, (1608/1024) * Double(1000 * 1000)) From 91f2cc1dfd9cc7ed5b66bab9b96fbf622bd5eb62 Mon Sep 17 00:00:00 2001 From: Ivan Karpan Date: Wed, 12 Nov 2025 09:58:15 +0000 Subject: [PATCH 2/3] fix(2785): remove comments per maintainer's request revert update SwiftLint config: rename redundant_optional_initialization to implicit_optional_initialization revert remove unnecessary SwiftLint disable comment in Charts.swift revert fix RAM test: update test case PID to avoid conflicts --- .swiftlint.yml | 2 +- Kit/plugins/Charts.swift | 1 + Modules/CPU/readers.swift | 7 ------ Tests/CPU.swift | 47 +++++++++++---------------------------- Tests/RAM.swift | 4 ++-- 5 files changed, 17 insertions(+), 44 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index ce3a13e0dc3..afd09fbc931 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -5,7 +5,7 @@ disabled_rules: - trailing_whitespace - opening_brace - implicit_getter - - implicit_optional_initialization + - redundant_optional_initialization - large_tuple - function_body_length diff --git a/Kit/plugins/Charts.swift b/Kit/plugins/Charts.swift index c0dfa782967..1ee0de3b1ae 100644 --- a/Kit/plugins/Charts.swift +++ b/Kit/plugins/Charts.swift @@ -852,6 +852,7 @@ public class BarChartView: NSView { let blocks: Int = 16 let spacing: CGFloat = 2 let count: CGFloat = CGFloat(values.count) + // swiftlint:disable:next empty_count guard count > 0, self.frame.width > 0, self.frame.height > 0 else { return } let partitionSize: CGSize = CGSize(width: (self.frame.width - (count*spacing)) / count, height: self.frame.height) diff --git a/Modules/CPU/readers.swift b/Modules/CPU/readers.swift index 352a8f424c5..e1fe74893db 100644 --- a/Modules/CPU/readers.swift +++ b/Modules/CPU/readers.swift @@ -12,9 +12,6 @@ import Cocoa import Kit -/// Calculate performance cores usage from cores and usagePerCore arrays -/// Uses positional matching (cores[i] -> usagePerCore[i]) to handle cases where CPU IDs don't match sequential indices -/// This fixes issue #2785 public func calculatePerformanceCoresUsage(cores: [core_s], usagePerCore: [Double]) -> Double? { guard cores.count == usagePerCore.count else { return nil } let pCoresList: [Double] = cores.enumerated().compactMap { (index, core) -> Double? in @@ -27,8 +24,6 @@ public func calculatePerformanceCoresUsage(cores: [core_s], usagePerCore: [Doubl return pCoresList.reduce(0, +) / Double(pCoresList.count) } -/// Calculate efficiency cores usage from cores and usagePerCore arrays -/// Uses positional matching (cores[i] -> usagePerCore[i]) to handle cases where CPU IDs don't match sequential indices public func calculateEfficiencyCoresUsage(cores: [core_s], usagePerCore: [Double]) -> Double? { guard cores.count == usagePerCore.count else { return nil } let eCoresList: [Double] = cores.enumerated().compactMap { (index, core) -> Double? in @@ -159,8 +154,6 @@ internal class LoadReader: Reader { self.response.totalUsage = self.response.systemLoad + self.response.userLoad if let cores = self.cores { - // Use positional matching instead of CPU ID to fix issue #2785 - // Match cores to usagePerCore by position in the array, not by CPU ID self.response.usageECores = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: self.response.usagePerCore) self.response.usagePCores = calculatePerformanceCoresUsage(cores: cores, usagePerCore: self.response.usagePerCore) } diff --git a/Tests/CPU.swift b/Tests/CPU.swift index 99b4861011d..82c3d95e429 100644 --- a/Tests/CPU.swift +++ b/Tests/CPU.swift @@ -14,7 +14,6 @@ import Kit class CPU: XCTestCase { - /// Helper function to create core_s instances using JSON decoding private func makeCore(id: Int32, type: coreType) -> core_s { let json = """ {"id": \(id), "type": \(type.rawValue)} @@ -26,22 +25,12 @@ class CPU: XCTestCase { return core } - /// Test that reproduces issue #2785: Performance cores showing 75% instead of 100% - /// This test demonstrates the bug scenario where CPU IDs don't match sequential indices - /// The test EXPECTS 100% (correct behavior) and will FAIL with buggy code showing 75% func testPerformanceCoresShouldShow100Percent_Issue2785() throws { - // Simulate M4 Pro 12/16: 4 efficiency + 8 performance cores = 12 total - // usagePerCore is built sequentially from host_processor_info (indices 0-11) - // All performance cores (indices 4-11) are at 100% utilization let usagePerCore: [Double] = [ - 0.5, 0.5, 0.5, 0.5, // indices 0-3: efficiency cores - 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 // indices 4-11: performance cores (ALL at 100%) + 0.5, 0.5, 0.5, 0.5, + 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ] - // BUG SCENARIO: Cores array has performance cores with IDs that don't all match sequential indices - // Some performance cores have IDs 4-9 (valid), but 2 have IDs 20,21 (out of bounds) - // This simulates what might happen when IORegistry returns CPU IDs that don't align - // with the sequential logical CPU indices from host_processor_info let cores: [core_s] = [ makeCore(id: 0, type: coreType.efficiency), makeCore(id: 1, type: coreType.efficiency), @@ -53,24 +42,18 @@ class CPU: XCTestCase { makeCore(id: 7, type: coreType.performance), makeCore(id: 8, type: coreType.performance), makeCore(id: 9, type: coreType.performance), - makeCore(id: 20, type: coreType.performance), // CPU ID 20 is out of bounds for usagePerCore[0-11] - makeCore(id: 21, type: coreType.performance) // CPU ID 21 is out of bounds for usagePerCore[0-11] + makeCore(id: 20, type: coreType.performance), + makeCore(id: 21, type: coreType.performance) ] - // Test the actual implementation function from LoadReader - // This tests the fix for issue #2785: positional matching instead of CPU ID matching let usagePCores = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) - // EXPECTATION: Should show 100% when all performance cores are at 100% - // With buggy ID-based code, this would show 75% (6 cores at 100%, 2 at 0%) instead of 100% XCTAssertNotNil(usagePCores, "Performance cores usage should be calculated") XCTAssertEqual(usagePCores!, 1.0, accuracy: 0.01, "When all 8 performance cores are at 100%, should show 100%, not 75%") XCTAssertEqual(cores.filter({ $0.type == .performance }).count, 8, "Should have 8 performance cores") } - // MARK: - calculatePerformanceCoresUsage Tests - func testCalculatePerformanceCoresUsage_AllCoresAt100Percent() throws { let cores: [core_s] = [ makeCore(id: 0, type: .efficiency), @@ -120,7 +103,7 @@ class CPU: XCTestCase { makeCore(id: 0, type: .performance), makeCore(id: 1, type: .performance) ] - let usagePerCore: [Double] = [1.0, 1.0, 1.0] // 3 elements, but only 2 cores + let usagePerCore: [Double] = [1.0, 1.0, 1.0] let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) @@ -137,14 +120,13 @@ class CPU: XCTestCase { } func testCalculatePerformanceCoresUsage_PositionalMatching() throws { - // Test that positional matching works correctly even with non-sequential IDs let cores: [core_s] = [ makeCore(id: 0, type: .efficiency), - makeCore(id: 5, type: .performance), // ID 5, but position 1 - makeCore(id: 10, type: .performance), // ID 10, but position 2 - makeCore(id: 20, type: .performance) // ID 20, but position 3 + makeCore(id: 5, type: .performance), + makeCore(id: 10, type: .performance), + makeCore(id: 20, type: .performance) ] - let usagePerCore: [Double] = [0.3, 0.8, 0.9, 1.0] // Position-based, not ID-based + let usagePerCore: [Double] = [0.3, 0.8, 0.9, 1.0] let result = calculatePerformanceCoresUsage(cores: cores, usagePerCore: usagePerCore) @@ -153,8 +135,6 @@ class CPU: XCTestCase { XCTAssertEqual(result!, expected, accuracy: 0.01, "Should use positional matching, not ID-based") } - // MARK: - calculateEfficiencyCoresUsage Tests - func testCalculateEfficiencyCoresUsage_AllCoresAt50Percent() throws { let cores: [core_s] = [ makeCore(id: 0, type: .efficiency), @@ -204,7 +184,7 @@ class CPU: XCTestCase { makeCore(id: 0, type: .efficiency), makeCore(id: 1, type: .efficiency) ] - let usagePerCore: [Double] = [0.5, 0.6, 0.7] // 3 elements, but only 2 cores + let usagePerCore: [Double] = [0.5, 0.6, 0.7] let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) @@ -212,13 +192,12 @@ class CPU: XCTestCase { } func testCalculateEfficiencyCoresUsage_PositionalMatching() throws { - // Test that positional matching works correctly even with non-sequential IDs let cores: [core_s] = [ - makeCore(id: 100, type: .efficiency), // ID 100, but position 0 - makeCore(id: 200, type: .efficiency), // ID 200, but position 1 + makeCore(id: 100, type: .efficiency), + makeCore(id: 200, type: .efficiency), makeCore(id: 300, type: .performance) ] - let usagePerCore: [Double] = [0.4, 0.6, 1.0] // Position-based, not ID-based + let usagePerCore: [Double] = [0.4, 0.6, 1.0] let result = calculateEfficiencyCoresUsage(cores: cores, usagePerCore: usagePerCore) diff --git a/Tests/RAM.swift b/Tests/RAM.swift index b827b0bfee1..2a9b1b83942 100644 --- a/Tests/RAM.swift +++ b/Tests/RAM.swift @@ -34,8 +34,8 @@ class RAM: XCTestCase { XCTAssertEqual(process.name, "NotificationCent") XCTAssertEqual(process.usage, 62 * Double(1000 * 1000)) - process = ProcessReader.parseProcess("99999 SafariCloudHisto 1608K") - XCTAssertEqual(process.pid, 99999) + process = ProcessReader.parseProcess("623 SafariCloudHisto 1608K") + XCTAssertEqual(process.pid, 623) XCTAssertEqual(process.name, "SafariCloudHisto") XCTAssertEqual(process.usage, (1608/1024) * Double(1000 * 1000)) From af724f3750b405dd317e7c0ce76d518a66b9e356 Mon Sep 17 00:00:00 2001 From: Ivan Karpan Date: Wed, 12 Nov 2025 11:52:11 +0000 Subject: [PATCH 3/3] chore(2785): update comment headers for the CPU tests file --- Tests/CPU.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/CPU.swift b/Tests/CPU.swift index 82c3d95e429..a97e0846041 100644 --- a/Tests/CPU.swift +++ b/Tests/CPU.swift @@ -2,10 +2,11 @@ // CPU.swift // Tests // -// Created to test CPU performance cores utilization calculation fix. -// Testing issue #2785: Performance cores showing 75% instead of 100% when fully utilized. +// Created by Ivan Karpan on 11/11/2025. +// Using Swift 5.0. +// Running on macOS 10.15. // -// Copyright © 2025. All rights reserved. +// Copyright © 2025 Serhiy Mytrovtsiy. All rights reserved. // import XCTest