View Javadoc
1   /*
2    * Copyright 2016-2024 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.hardware.platform.unix.freebsd;
6   
7   import java.util.ArrayList;
8   import java.util.HashMap;
9   import java.util.HashSet;
10  import java.util.List;
11  import java.util.Locale;
12  import java.util.Map;
13  import java.util.Set;
14  import java.util.regex.Matcher;
15  import java.util.regex.Pattern;
16  
17  import org.slf4j.Logger;
18  import org.slf4j.LoggerFactory;
19  
20  import com.sun.jna.Memory;
21  import com.sun.jna.Native;
22  import com.sun.jna.platform.unix.LibCAPI.size_t;
23  
24  import oshi.annotation.concurrent.ThreadSafe;
25  import oshi.hardware.CentralProcessor.ProcessorCache.Type;
26  import oshi.hardware.common.AbstractCentralProcessor;
27  import oshi.jna.ByRef.CloseableSizeTByReference;
28  import oshi.jna.platform.unix.FreeBsdLibc;
29  import oshi.jna.platform.unix.FreeBsdLibc.CpTime;
30  import oshi.util.ExecutingCommand;
31  import oshi.util.FileUtil;
32  import oshi.util.ParseUtil;
33  import oshi.util.platform.unix.freebsd.BsdSysctlUtil;
34  import oshi.util.tuples.Quartet;
35  
36  /**
37   * A CPU
38   */
39  @ThreadSafe
40  final class FreeBsdCentralProcessor extends AbstractCentralProcessor {
41  
42      private static final Logger LOG = LoggerFactory.getLogger(FreeBsdCentralProcessor.class);
43  
44      // Capture the CSV of hex values as group(1), clients should split on ','
45      private static final Pattern CPUMASK = Pattern
46              .compile(".*<cpu\\s.*mask=\"(\\p{XDigit}+(,\\p{XDigit}+)*)\".*>.*</cpu>.*");
47  
48      private static final long CPTIME_SIZE;
49      static {
50          try (CpTime cpTime = new CpTime()) {
51              CPTIME_SIZE = cpTime.size();
52          }
53      }
54  
55      @Override
56      protected ProcessorIdentifier queryProcessorId() {
57          final Pattern identifierPattern = Pattern
58                  .compile("Origin=\"([^\"]*)\".*Id=(\\S+).*Family=(\\S+).*Model=(\\S+).*Stepping=(\\S+).*");
59          final Pattern featuresPattern = Pattern.compile("Features=(\\S+)<.*");
60  
61          String cpuVendor = "";
62          String cpuName = BsdSysctlUtil.sysctl("hw.model", "");
63          String cpuFamily = "";
64          String cpuModel = "";
65          String cpuStepping = "";
66          String processorID;
67          long cpuFreq = BsdSysctlUtil.sysctl("hw.clockrate", 0L) * 1_000_000L;
68  
69          boolean cpu64bit;
70  
71          // Parsing dmesg.boot is apparently the only reliable source for processor
72          // identification in FreeBSD
73          long processorIdBits = 0L;
74          List<String> cpuInfo = FileUtil.readFile("/var/run/dmesg.boot");
75          for (String line : cpuInfo) {
76              line = line.trim();
77              // Prefer hw.model to this one
78              if (line.startsWith("CPU:") && cpuName.isEmpty()) {
79                  cpuName = line.replace("CPU:", "").trim();
80              } else if (line.startsWith("Origin=")) {
81                  Matcher m = identifierPattern.matcher(line);
82                  if (m.matches()) {
83                      cpuVendor = m.group(1);
84                      processorIdBits |= Long.decode(m.group(2));
85                      cpuFamily = Integer.decode(m.group(3)).toString();
86                      cpuModel = Integer.decode(m.group(4)).toString();
87                      cpuStepping = Integer.decode(m.group(5)).toString();
88                  }
89              } else if (line.startsWith("Features=")) {
90                  Matcher m = featuresPattern.matcher(line);
91                  if (m.matches()) {
92                      processorIdBits |= Long.decode(m.group(1)) << 32;
93                  }
94                  // No further interest in this file
95                  break;
96              }
97          }
98          cpu64bit = ExecutingCommand.getFirstAnswer("uname -m").trim().contains("64");
99          processorID = getProcessorIDfromDmiDecode(processorIdBits);
100 
101         return new ProcessorIdentifier(cpuVendor, cpuName, cpuFamily, cpuModel, cpuStepping, processorID, cpu64bit,
102                 cpuFreq);
103     }
104 
105     @Override
106     protected Quartet<List<LogicalProcessor>, List<PhysicalProcessor>, List<ProcessorCache>, List<String>> initProcessorCounts() {
107         List<LogicalProcessor> logProcs = parseTopology();
108         // Force at least one processor
109         if (logProcs.isEmpty()) {
110             logProcs.add(new LogicalProcessor(0, 0, 0));
111         }
112         Map<Integer, String> dmesg = new HashMap<>();
113         // cpu0: <Open Firmware CPU> on cpulist0
114         Pattern normal = Pattern.compile("cpu(\\\\d+): (.+) on .*");
115         // CPU 0: ARM Cortex-A53 r0p4 affinity: 0 0
116         Pattern hybrid = Pattern.compile("CPU\\\\s*(\\\\d+): (.+) affinity:.*");
117         List<String> featureFlags = new ArrayList<>();
118         boolean readingFlags = false;
119         for (String s : FileUtil.readFile("/var/run/dmesg.boot")) {
120             Matcher h = hybrid.matcher(s);
121             if (h.matches()) {
122                 int coreId = ParseUtil.parseIntOrDefault(h.group(1), 0);
123                 // This always takes priority, overwrite if needed
124                 dmesg.put(coreId, h.group(2).trim());
125             } else {
126                 Matcher n = normal.matcher(s);
127                 if (n.matches()) {
128                     int coreId = ParseUtil.parseIntOrDefault(n.group(1), 0);
129                     // Don't overwrite if h matched earlier
130                     dmesg.putIfAbsent(coreId, n.group(2).trim());
131                 }
132             }
133             if (s.contains("Origin=")) {
134                 readingFlags = true;
135             } else if (readingFlags) {
136                 if (s.startsWith("  ")) {
137                     featureFlags.add(s.trim());
138                 } else {
139                     readingFlags = false;
140                 }
141             }
142         }
143         List<PhysicalProcessor> physProcs = dmesg.isEmpty() ? null : createProcListFromDmesg(logProcs, dmesg);
144         List<ProcessorCache> caches = getCacheInfoFromLscpu();
145         return new Quartet<>(logProcs, physProcs, caches, featureFlags);
146     }
147 
148     private List<ProcessorCache> getCacheInfoFromLscpu() {
149         Set<ProcessorCache> caches = new HashSet<>();
150         for (String checkLine : ExecutingCommand.runNative("lscpu")) {
151             if (checkLine.contains("L1d cache:")) {
152                 caches.add(new ProcessorCache(1, 0, 0,
153                         ParseUtil.parseDecimalMemorySizeToBinary(checkLine.split(":")[1].trim()), Type.DATA));
154             } else if (checkLine.contains("L1i cache:")) {
155                 caches.add(new ProcessorCache(1, 0, 0,
156                         ParseUtil.parseDecimalMemorySizeToBinary(checkLine.split(":")[1].trim()), Type.INSTRUCTION));
157             } else if (checkLine.contains("L2 cache:")) {
158                 caches.add(new ProcessorCache(2, 0, 0,
159                         ParseUtil.parseDecimalMemorySizeToBinary(checkLine.split(":")[1].trim()), Type.UNIFIED));
160             } else if (checkLine.contains("L3 cache:")) {
161                 caches.add(new ProcessorCache(3, 0, 0,
162                         ParseUtil.parseDecimalMemorySizeToBinary(checkLine.split(":")[1].trim()), Type.UNIFIED));
163             }
164         }
165         return orderedProcCaches(caches);
166     }
167 
168     private static List<LogicalProcessor> parseTopology() {
169         String[] topology = BsdSysctlUtil.sysctl("kern.sched.topology_spec", "").split("[\\n\\r]");
170         /*-
171          * Sample output:
172          *
173         <groups>
174         <group level="1" cache-level="0">
175          <cpu count="24" mask="ffffff">0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23</cpu>
176          <children>
177           <group level="2" cache-level="2">
178            <cpu count="12" mask="fff">0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11</cpu>
179            <children>
180             <group level="3" cache-level="1">
181              <cpu count="2" mask="3">0, 1</cpu>
182              <flags><flag name="THREAD">THREAD group</flag><flag name="SMT">SMT group</flag></flags>
183             </group>
184             ...
185         * On FreeBSD 13.1, the output may contain a csv value for the mask:
186         <groups>
187          <group level="1" cache-level="3">
188           <cpu count="8" mask="ff,0,0,0">0, 1, 2, 3, 4, 5, 6, 7</cpu>
189           <children>
190            <group level="2" cache-level="2">
191             <cpu count="2" mask="3,0,0,0">0, 1</cpu>
192             ...
193         *
194         * Opens with <groups>
195         * <group> level 1 identifies all the processors via bitmask, should only be one
196         * <group> level 2 separates by physical package
197         * <group> level 3 puts hyperthreads together: if THREAD or SMT or HTT all the CPUs are one physical
198         * If there is no level 3, then all logical processors are physical
199         */
200         // Create lists of the group bitmasks
201         long group1 = 1L;
202         List<Long> group2 = new ArrayList<>();
203         List<Long> group3 = new ArrayList<>();
204         int groupLevel = 0;
205         for (String topo : topology) {
206             if (topo.contains("<group level=")) {
207                 groupLevel++;
208             } else if (topo.contains("</group>")) {
209                 groupLevel--;
210             } else if (topo.contains("<cpu")) {
211                 // Find <cpu> tag and extract bits
212                 Matcher m = CPUMASK.matcher(topo);
213                 if (m.matches()) {
214                     // If csv of hex values like "f,0,0,0", parse the first value
215                     String csvMatch = m.group(1);
216                     String[] csvTokens = csvMatch.split(",");
217                     String firstVal = csvTokens[0];
218 
219                     // Regex guarantees parsing digits so we won't get a
220                     // NumberFormatException
221                     long parsedVal = ParseUtil.hexStringToLong(firstVal, 0);
222                     switch (groupLevel) {
223                     case 1:
224                         group1 = parsedVal;
225                         break;
226                     case 2:
227                         group2.add(parsedVal);
228                         break;
229                     case 3:
230                         group3.add(parsedVal);
231                         break;
232                     default:
233                         break;
234                     }
235                 }
236             }
237         }
238         return matchBitmasks(group1, group2, group3);
239     }
240 
241     private static List<LogicalProcessor> matchBitmasks(long group1, List<Long> group2, List<Long> group3) {
242         List<LogicalProcessor> logProcs = new ArrayList<>();
243         // Lowest and Highest set bits, indexing from 0
244         int lowBit = Long.numberOfTrailingZeros(group1);
245         int hiBit = 63 - Long.numberOfLeadingZeros(group1);
246         // Create logical processors for this core
247         for (int i = lowBit; i <= hiBit; i++) {
248             if ((group1 & (1L << i)) > 0) {
249                 int numaNode = 0;
250                 LogicalProcessor logProc = new LogicalProcessor(i, getMatchingBitmask(group3, i),
251                         getMatchingBitmask(group2, i), numaNode);
252                 logProcs.add(logProc);
253             }
254         }
255         return logProcs;
256     }
257 
258     private static int getMatchingBitmask(List<Long> bitmasks, int lp) {
259         for (int j = 0; j < bitmasks.size(); j++) {
260             if ((bitmasks.get(j).longValue() & (1L << lp)) != 0) {
261                 return j;
262             }
263         }
264         return 0;
265     }
266 
267     @Override
268     public long[] querySystemCpuLoadTicks() {
269         long[] ticks = new long[TickType.values().length];
270         try (CpTime cpTime = new CpTime()) {
271             BsdSysctlUtil.sysctl("kern.cp_time", cpTime);
272             ticks[TickType.USER.getIndex()] = cpTime.cpu_ticks[FreeBsdLibc.CP_USER];
273             ticks[TickType.NICE.getIndex()] = cpTime.cpu_ticks[FreeBsdLibc.CP_NICE];
274             ticks[TickType.SYSTEM.getIndex()] = cpTime.cpu_ticks[FreeBsdLibc.CP_SYS];
275             ticks[TickType.IRQ.getIndex()] = cpTime.cpu_ticks[FreeBsdLibc.CP_INTR];
276             ticks[TickType.IDLE.getIndex()] = cpTime.cpu_ticks[FreeBsdLibc.CP_IDLE];
277         }
278         return ticks;
279     }
280 
281     @Override
282     public long[] queryCurrentFreq() {
283         long[] freq = new long[1];
284         freq[0] = BsdSysctlUtil.sysctl("dev.cpu.0.freq", -1L);
285         if (freq[0] > 0) {
286             // If success, value is in MHz
287             freq[0] *= 1_000_000L;
288         } else {
289             freq[0] = BsdSysctlUtil.sysctl("machdep.tsc_freq", -1L);
290         }
291         return freq;
292     }
293 
294     @Override
295     public long queryMaxFreq() {
296         long max = -1L;
297         String freqLevels = BsdSysctlUtil.sysctl("dev.cpu.0.freq_levels", "");
298         // MHz/Watts pairs like: 2501/32000 2187/27125 2000/24000
299         for (String s : ParseUtil.whitespaces.split(freqLevels)) {
300             long freq = ParseUtil.parseLongOrDefault(s.split("/")[0], -1L);
301             if (max < freq) {
302                 max = freq;
303             }
304         }
305         if (max > 0) {
306             // If success, value is in MHz
307             max *= 1_000_000;
308         } else {
309             max = BsdSysctlUtil.sysctl("machdep.tsc_freq", -1L);
310         }
311         return max;
312     }
313 
314     @Override
315     public double[] getSystemLoadAverage(int nelem) {
316         if (nelem < 1 || nelem > 3) {
317             throw new IllegalArgumentException("Must include from one to three elements.");
318         }
319         double[] average = new double[nelem];
320         int retval = FreeBsdLibc.INSTANCE.getloadavg(average, nelem);
321         if (retval < nelem) {
322             for (int i = Math.max(retval, 0); i < average.length; i++) {
323                 average[i] = -1d;
324             }
325         }
326         return average;
327     }
328 
329     @Override
330     public long[][] queryProcessorCpuLoadTicks() {
331         long[][] ticks = new long[getLogicalProcessorCount()][TickType.values().length];
332 
333         // Allocate memory for array of CPTime
334         long arraySize = CPTIME_SIZE * getLogicalProcessorCount();
335         try (Memory p = new Memory(arraySize);
336                 CloseableSizeTByReference oldlenp = new CloseableSizeTByReference(arraySize)) {
337             String name = "kern.cp_times";
338             // Fetch
339             if (0 != FreeBsdLibc.INSTANCE.sysctlbyname(name, p, oldlenp, null, size_t.ZERO)) {
340                 LOG.error("Failed sysctl call: {}, Error code: {}", name, Native.getLastError());
341                 return ticks;
342             }
343             // p now points to the data; need to copy each element
344             for (int cpu = 0; cpu < getLogicalProcessorCount(); cpu++) {
345                 ticks[cpu][TickType.USER.getIndex()] = p
346                         .getLong(CPTIME_SIZE * cpu + FreeBsdLibc.CP_USER * FreeBsdLibc.UINT64_SIZE); // lgtm
347                 ticks[cpu][TickType.NICE.getIndex()] = p
348                         .getLong(CPTIME_SIZE * cpu + FreeBsdLibc.CP_NICE * FreeBsdLibc.UINT64_SIZE); // lgtm
349                 ticks[cpu][TickType.SYSTEM.getIndex()] = p
350                         .getLong(CPTIME_SIZE * cpu + FreeBsdLibc.CP_SYS * FreeBsdLibc.UINT64_SIZE); // lgtm
351                 ticks[cpu][TickType.IRQ.getIndex()] = p
352                         .getLong(CPTIME_SIZE * cpu + FreeBsdLibc.CP_INTR * FreeBsdLibc.UINT64_SIZE); // lgtm
353                 ticks[cpu][TickType.IDLE.getIndex()] = p
354                         .getLong(CPTIME_SIZE * cpu + FreeBsdLibc.CP_IDLE * FreeBsdLibc.UINT64_SIZE); // lgtm
355             }
356         }
357         return ticks;
358     }
359 
360     /**
361      * Fetches the ProcessorID from dmidecode (if possible with root permissions), otherwise uses the values from
362      * /var/run/dmesg.boot
363      *
364      * @param processorID The processorID as a long
365      * @return The ProcessorID string
366      */
367     private static String getProcessorIDfromDmiDecode(long processorID) {
368         boolean procInfo = false;
369         String marker = "Processor Information";
370         for (String checkLine : ExecutingCommand.runNative("dmidecode -t system")) {
371             if (!procInfo && checkLine.contains(marker)) {
372                 marker = "ID:";
373                 procInfo = true;
374             } else if (procInfo && checkLine.contains(marker)) {
375                 return checkLine.split(marker)[1].trim();
376             }
377         }
378         // If we've gotten this far, dmidecode failed. Used the passed-in values
379         return String.format(Locale.ROOT, "%016X", processorID);
380     }
381 
382     @Override
383     public long queryContextSwitches() {
384         String name = "vm.stats.sys.v_swtch";
385         size_t.ByReference size = new size_t.ByReference(new size_t(FreeBsdLibc.INT_SIZE));
386         try (Memory p = new Memory(size.longValue())) {
387             if (0 != FreeBsdLibc.INSTANCE.sysctlbyname(name, p, size, null, size_t.ZERO)) {
388                 return 0L;
389             }
390             return ParseUtil.unsignedIntToLong(p.getInt(0));
391         }
392     }
393 
394     @Override
395     public long queryInterrupts() {
396         String name = "vm.stats.sys.v_intr";
397         size_t.ByReference size = new size_t.ByReference(new size_t(FreeBsdLibc.INT_SIZE));
398         try (Memory p = new Memory(size.longValue())) {
399             if (0 != FreeBsdLibc.INSTANCE.sysctlbyname(name, p, size, null, size_t.ZERO)) {
400                 return 0L;
401             }
402             return ParseUtil.unsignedIntToLong(p.getInt(0));
403         }
404     }
405 }