View Javadoc
1   /*
2    * Copyright 2016-2024 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.hardware.platform.mac;
6   
7   import static oshi.util.Memoizer.memoize;
8   
9   import java.nio.charset.StandardCharsets;
10  import java.util.ArrayList;
11  import java.util.Arrays;
12  import java.util.HashMap;
13  import java.util.HashSet;
14  import java.util.List;
15  import java.util.Locale;
16  import java.util.Map;
17  import java.util.Objects;
18  import java.util.Set;
19  import java.util.function.Supplier;
20  import java.util.regex.Matcher;
21  import java.util.regex.Pattern;
22  import java.util.stream.Collectors;
23  import java.util.stream.Stream;
24  
25  import org.slf4j.Logger;
26  import org.slf4j.LoggerFactory;
27  
28  import com.sun.jna.Native;
29  import com.sun.jna.platform.mac.IOKit.IOIterator;
30  import com.sun.jna.platform.mac.IOKit.IORegistryEntry;
31  import com.sun.jna.platform.mac.IOKitUtil;
32  import com.sun.jna.platform.mac.SystemB;
33  
34  import oshi.annotation.concurrent.ThreadSafe;
35  import oshi.hardware.CentralProcessor.ProcessorCache.Type;
36  import oshi.hardware.common.AbstractCentralProcessor;
37  import oshi.jna.ByRef.CloseableIntByReference;
38  import oshi.jna.ByRef.CloseablePointerByReference;
39  import oshi.jna.Struct.CloseableHostCpuLoadInfo;
40  import oshi.util.ExecutingCommand;
41  import oshi.util.FormatUtil;
42  import oshi.util.ParseUtil;
43  import oshi.util.Util;
44  import oshi.util.platform.mac.SysctlUtil;
45  import oshi.util.tuples.Quartet;
46  
47  /**
48   * A CPU.
49   */
50  @ThreadSafe
51  final class MacCentralProcessor extends AbstractCentralProcessor {
52  
53      private static final Logger LOG = LoggerFactory.getLogger(MacCentralProcessor.class);
54  
55      private static final Set<String> ARM_P_CORES = Stream
56              .of("apple,firestorm arm,v8", "apple,avalanche arm,v8", "apple,everest arm,v8").collect(Collectors.toSet());
57  
58      private static final int ARM_CPUTYPE = 0x0100000C;
59      private static final int M1_CPUFAMILY = 0x1b588bb3;
60      private static final int M2_CPUFAMILY = 0xda33d83d;
61      private static final int M3_CPUFAMILY = 0x8765edea;
62      private static final long DEFAULT_FREQUENCY = 2_400_000_000L;
63      private static final Pattern CPU_N = Pattern.compile("^cpu(\\d+)");
64  
65      private final Supplier<String> vendor = memoize(MacCentralProcessor::platformExpert);
66      private final boolean isArmCpu = isArmCpu();
67  
68      // Equivalents of hw.cpufrequency on Apple Silicon, defaulting to Rosetta value
69      // Will update during initialization
70      private long performanceCoreFrequency = DEFAULT_FREQUENCY;
71      private long efficiencyCoreFrequency = DEFAULT_FREQUENCY;
72  
73      @Override
74      protected ProcessorIdentifier queryProcessorId() {
75          String cpuName = SysctlUtil.sysctl("machdep.cpu.brand_string", "");
76          String cpuVendor;
77          String cpuStepping;
78          String cpuModel;
79          String cpuFamily;
80          String processorID;
81          // Initial M1 chips said "Apple Processor". Later branding includes M1, M1 Pro,
82          // M1 Max, M2, etc. So if it starts with Apple it's M-something.
83          if (cpuName.startsWith("Apple")) {
84              // Processing an M1 chip
85              cpuVendor = vendor.get();
86              cpuStepping = "0"; // No correlation yet
87              cpuModel = "0"; // No correlation yet
88              int type;
89              int family;
90              if (isArmCpu) {
91                  type = ARM_CPUTYPE;
92                  int mSeries = ParseUtil.getFirstIntValue(cpuName);
93                  switch (mSeries) {
94                  case 2:
95                      family = M2_CPUFAMILY;
96                      break;
97                  case 3:
98                      family = M3_CPUFAMILY;
99                      break;
100                 default:
101                     // Some M1 did not brand as such
102                     family = M1_CPUFAMILY;
103                 }
104             } else {
105                 type = SysctlUtil.sysctl("hw.cputype", 0);
106                 family = SysctlUtil.sysctl("hw.cpufamily", 0);
107             }
108             // Translate to output
109             cpuFamily = String.format(Locale.ROOT, "0x%08x", family);
110             // Processor ID is an intel concept but CPU type + family conveys same info
111             processorID = String.format(Locale.ROOT, "%08x%08x", type, family);
112         } else {
113             // Processing an Intel chip
114             cpuVendor = SysctlUtil.sysctl("machdep.cpu.vendor", "");
115             int i = SysctlUtil.sysctl("machdep.cpu.stepping", -1);
116             cpuStepping = i < 0 ? "" : Integer.toString(i);
117             i = SysctlUtil.sysctl("machdep.cpu.model", -1);
118             cpuModel = i < 0 ? "" : Integer.toString(i);
119             i = SysctlUtil.sysctl("machdep.cpu.family", -1);
120             cpuFamily = i < 0 ? "" : Integer.toString(i);
121             long processorIdBits = 0L;
122             processorIdBits |= SysctlUtil.sysctl("machdep.cpu.signature", 0);
123             processorIdBits |= (SysctlUtil.sysctl("machdep.cpu.feature_bits", 0L) & 0xffffffff) << 32;
124             processorID = String.format(Locale.ROOT, "%016x", processorIdBits);
125         }
126         if (isArmCpu) {
127             calculateNominalFrequencies();
128         }
129         long cpuFreq = isArmCpu ? performanceCoreFrequency : SysctlUtil.sysctl("hw.cpufrequency", 0L);
130         boolean cpu64bit = SysctlUtil.sysctl("hw.cpu64bit_capable", 0) != 0;
131 
132         return new ProcessorIdentifier(cpuVendor, cpuName, cpuFamily, cpuModel, cpuStepping, processorID, cpu64bit,
133                 cpuFreq);
134     }
135 
136     @Override
137     protected Quartet<List<LogicalProcessor>, List<PhysicalProcessor>, List<ProcessorCache>, List<String>> initProcessorCounts() {
138         int logicalProcessorCount = SysctlUtil.sysctl("hw.logicalcpu", 1);
139         int physicalProcessorCount = SysctlUtil.sysctl("hw.physicalcpu", 1);
140         int physicalPackageCount = SysctlUtil.sysctl("hw.packages", 1);
141         List<LogicalProcessor> logProcs = new ArrayList<>(logicalProcessorCount);
142         Set<Integer> pkgCoreKeys = new HashSet<>();
143         for (int i = 0; i < logicalProcessorCount; i++) {
144             int coreId = i * physicalProcessorCount / logicalProcessorCount;
145             int pkgId = i * physicalPackageCount / logicalProcessorCount;
146             logProcs.add(new LogicalProcessor(i, coreId, pkgId));
147             pkgCoreKeys.add((pkgId << 16) + coreId);
148         }
149         Map<Integer, String> compatMap = queryCompatibleStrings();
150         int perflevels = SysctlUtil.sysctl("hw.nperflevels", 1);
151         List<PhysicalProcessor> physProcs = pkgCoreKeys.stream().sorted().map(k -> {
152             String compat = compatMap.getOrDefault(k, "").toLowerCase(Locale.ROOT);
153             // This is brittle. A better long term solution is to use sysctls
154             // hw.perflevel1.physicalcpu: 2
155             // hw.perflevel0.physicalcpu: 8
156             // Note the 1 and 0 values are reversed from OSHI API definition
157             int efficiency = ARM_P_CORES.contains(compat) ? 1 : 0;
158             return new PhysicalProcessor(k >> 16, k & 0xffff, efficiency, compat);
159         }).collect(Collectors.toList());
160         List<ProcessorCache> caches = orderedProcCaches(getCacheValues(perflevels));
161         List<String> featureFlags = getFeatureFlagsFromSysctl();
162         return new Quartet<>(logProcs, physProcs, caches, featureFlags);
163     }
164 
165     private Set<ProcessorCache> getCacheValues(int perflevels) {
166         int linesize = (int) SysctlUtil.sysctl("hw.cachelinesize", 0L);
167         int l1associativity = SysctlUtil.sysctl("machdep.cpu.cache.L1_associativity", 0, false);
168         int l2associativity = SysctlUtil.sysctl("machdep.cpu.cache.L2_associativity", 0, false);
169         Set<ProcessorCache> caches = new HashSet<>();
170         for (int i = 0; i < perflevels; i++) {
171             int size = SysctlUtil.sysctl("hw.perflevel" + i + ".l1icachesize", 0, false);
172             if (size > 0) {
173                 caches.add(new ProcessorCache(1, l1associativity, linesize, size, Type.INSTRUCTION));
174             }
175             size = SysctlUtil.sysctl("hw.perflevel" + i + ".l1dcachesize", 0, false);
176             if (size > 0) {
177                 caches.add(new ProcessorCache(1, l1associativity, linesize, size, Type.DATA));
178             }
179             size = SysctlUtil.sysctl("hw.perflevel" + i + ".l2cachesize", 0, false);
180             if (size > 0) {
181                 caches.add(new ProcessorCache(2, l2associativity, linesize, size, Type.UNIFIED));
182             }
183             size = SysctlUtil.sysctl("hw.perflevel" + i + ".l3cachesize", 0, false);
184             if (size > 0) {
185                 caches.add(new ProcessorCache(3, 0, linesize, size, Type.UNIFIED));
186             }
187         }
188         return caches;
189     }
190 
191     private List<String> getFeatureFlagsFromSysctl() {
192         List<String> x86Features = Arrays.asList("features", "extfeatures", "leaf7_features").stream().map(f -> {
193             String key = "machdep.cpu." + f;
194             String features = SysctlUtil.sysctl(key, "", false);
195             return Util.isBlank(features) ? null : (key + ": " + features);
196         }).filter(Objects::nonNull).collect(Collectors.toList());
197         return x86Features.isEmpty() ? ExecutingCommand.runNative("sysctl -a hw.optional") : x86Features;
198     }
199 
200     @Override
201     public long[] querySystemCpuLoadTicks() {
202         long[] ticks = new long[TickType.values().length];
203         int machPort = SystemB.INSTANCE.mach_host_self();
204         try (CloseableHostCpuLoadInfo cpuLoadInfo = new CloseableHostCpuLoadInfo();
205                 CloseableIntByReference size = new CloseableIntByReference(cpuLoadInfo.size())) {
206             if (0 != SystemB.INSTANCE.host_statistics(machPort, SystemB.HOST_CPU_LOAD_INFO, cpuLoadInfo, size)) {
207                 LOG.error("Failed to get System CPU ticks. Error code: {} ", Native.getLastError());
208                 return ticks;
209             }
210 
211             ticks[TickType.USER.getIndex()] = cpuLoadInfo.cpu_ticks[SystemB.CPU_STATE_USER];
212             ticks[TickType.NICE.getIndex()] = cpuLoadInfo.cpu_ticks[SystemB.CPU_STATE_NICE];
213             ticks[TickType.SYSTEM.getIndex()] = cpuLoadInfo.cpu_ticks[SystemB.CPU_STATE_SYSTEM];
214             ticks[TickType.IDLE.getIndex()] = cpuLoadInfo.cpu_ticks[SystemB.CPU_STATE_IDLE];
215         }
216         // Leave IOWait and IRQ values as 0
217         return ticks;
218     }
219 
220     @Override
221     public long[] queryCurrentFreq() {
222         if (isArmCpu) {
223             Map<Integer, Long> physFreqMap = new HashMap<>();
224             getPhysicalProcessors().stream().forEach(p -> physFreqMap.put(p.getPhysicalProcessorNumber(),
225                     p.getEfficiency() > 0 ? performanceCoreFrequency : efficiencyCoreFrequency));
226             return getLogicalProcessors().stream().map(LogicalProcessor::getPhysicalProcessorNumber)
227                     .map(p -> physFreqMap.getOrDefault(p, performanceCoreFrequency)).mapToLong(f -> f).toArray();
228         }
229         return new long[] { getProcessorIdentifier().getVendorFreq() };
230     }
231 
232     @Override
233     public long queryMaxFreq() {
234         if (isArmCpu) {
235             return performanceCoreFrequency;
236         }
237         return SysctlUtil.sysctl("hw.cpufrequency_max", getProcessorIdentifier().getVendorFreq());
238     }
239 
240     @Override
241     public double[] getSystemLoadAverage(int nelem) {
242         if (nelem < 1 || nelem > 3) {
243             throw new IllegalArgumentException("Must include from one to three elements.");
244         }
245         double[] average = new double[nelem];
246         int retval = SystemB.INSTANCE.getloadavg(average, nelem);
247         if (retval < nelem) {
248             Arrays.fill(average, -1d);
249         }
250         return average;
251     }
252 
253     @Override
254     public long[][] queryProcessorCpuLoadTicks() {
255         long[][] ticks = new long[getLogicalProcessorCount()][TickType.values().length];
256 
257         int machPort = SystemB.INSTANCE.mach_host_self();
258         try (CloseableIntByReference procCount = new CloseableIntByReference();
259                 CloseablePointerByReference procCpuLoadInfo = new CloseablePointerByReference();
260                 CloseableIntByReference procInfoCount = new CloseableIntByReference()) {
261             if (0 != SystemB.INSTANCE.host_processor_info(machPort, SystemB.PROCESSOR_CPU_LOAD_INFO, procCount,
262                     procCpuLoadInfo, procInfoCount)) {
263                 LOG.error("Failed to update CPU Load. Error code: {}", Native.getLastError());
264                 return ticks;
265             }
266 
267             int[] cpuTicks = procCpuLoadInfo.getValue().getIntArray(0, procInfoCount.getValue());
268             for (int cpu = 0; cpu < procCount.getValue(); cpu++) {
269                 int offset = cpu * SystemB.CPU_STATE_MAX;
270                 ticks[cpu][TickType.USER.getIndex()] = FormatUtil
271                         .getUnsignedInt(cpuTicks[offset + SystemB.CPU_STATE_USER]);
272                 ticks[cpu][TickType.NICE.getIndex()] = FormatUtil
273                         .getUnsignedInt(cpuTicks[offset + SystemB.CPU_STATE_NICE]);
274                 ticks[cpu][TickType.SYSTEM.getIndex()] = FormatUtil
275                         .getUnsignedInt(cpuTicks[offset + SystemB.CPU_STATE_SYSTEM]);
276                 ticks[cpu][TickType.IDLE.getIndex()] = FormatUtil
277                         .getUnsignedInt(cpuTicks[offset + SystemB.CPU_STATE_IDLE]);
278             }
279         }
280         return ticks;
281     }
282 
283     @Override
284     public long queryContextSwitches() {
285         // Not available on macOS since at least 10.3.9. Early versions may have
286         // provided access to the vmmeter structure using sysctl [CTL_VM, VM_METER] but
287         // it now fails (ENOENT) and there is no other reference to it in source code
288         return 0L;
289     }
290 
291     @Override
292     public long queryInterrupts() {
293         // Not available on macOS since at least 10.3.9. Early versions may have
294         // provided access to the vmmeter structure using sysctl [CTL_VM, VM_METER] but
295         // it now fails (ENOENT) and there is no other reference to it in source code
296         return 0L;
297     }
298 
299     private static String platformExpert() {
300         String manufacturer = null;
301         IORegistryEntry platformExpert = IOKitUtil.getMatchingService("IOPlatformExpertDevice");
302         if (platformExpert != null) {
303             // Get manufacturer from IOPlatformExpertDevice
304             byte[] data = platformExpert.getByteArrayProperty("manufacturer");
305             if (data != null) {
306                 manufacturer = Native.toString(data, StandardCharsets.UTF_8);
307             }
308             platformExpert.release();
309         }
310         return Util.isBlank(manufacturer) ? "Apple Inc." : manufacturer;
311     }
312 
313     // Called by initProcessorCount in the constructor
314     // These populate the physical processor id strings
315     private static Map<Integer, String> queryCompatibleStrings() {
316         Map<Integer, String> compatibleStrMap = new HashMap<>();
317         // All CPUs are an IOPlatformDevice
318         // Iterate each CPU and save frequency and "compatible" strings
319         IOIterator iter = IOKitUtil.getMatchingServices("IOPlatformDevice");
320         if (iter != null) {
321             IORegistryEntry cpu = iter.next();
322             while (cpu != null) {
323                 Matcher m = CPU_N.matcher(cpu.getName().toLowerCase(Locale.ROOT));
324                 if (m.matches()) {
325                     int procId = ParseUtil.parseIntOrDefault(m.group(1), 0);
326                     // Compatible key is null-delimited C string array in byte array
327                     byte[] data = cpu.getByteArrayProperty("compatible");
328                     if (data != null) {
329                         // Byte array is null delimited
330                         // Example value for M2: "apple,blizzard", "ARM,v8"
331                         compatibleStrMap.put(procId,
332                                 new String(data, StandardCharsets.UTF_8).replace('\0', ' ').trim());
333                     }
334                 }
335                 cpu.release();
336                 cpu = iter.next();
337             }
338             iter.release();
339         }
340         return compatibleStrMap;
341     }
342 
343     // Called when initiating instance variables which occurs after constructor has
344     // populated physical processors
345     private boolean isArmCpu() {
346         return getPhysicalProcessors().stream().map(PhysicalProcessor::getIdString).anyMatch(id -> id.contains("arm"));
347     }
348 
349     private void calculateNominalFrequencies() {
350         IOIterator iter = IOKitUtil.getMatchingServices("AppleARMIODevice");
351         if (iter != null) {
352             try {
353                 IORegistryEntry device = iter.next();
354                 try {
355                     while (device != null) {
356                         if (device.getName().equalsIgnoreCase("pmgr")) {
357                             performanceCoreFrequency = getMaxFreqFromByteArray(
358                                     device.getByteArrayProperty("voltage-states5-sram"));
359                             efficiencyCoreFrequency = getMaxFreqFromByteArray(
360                                     device.getByteArrayProperty("voltage-states1-sram"));
361                             return;
362                         }
363                         device.release();
364                         device = iter.next();
365                     }
366                 } finally {
367                     if (device != null) {
368                         device.release();
369                     }
370                 }
371             } finally {
372                 iter.release();
373             }
374         }
375     }
376 
377     private long getMaxFreqFromByteArray(byte[] data) {
378         // Max freq is 8 bytes from the end of the array
379         if (data != null && data.length >= 8) {
380             byte[] freqData = Arrays.copyOfRange(data, data.length - 8, data.length - 4);
381             return ParseUtil.byteArrayToLong(freqData, 4, false);
382         }
383         return DEFAULT_FREQUENCY;
384     }
385 }