• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2023 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "base/process/process_metrics.h"
6 
7 #include <AvailabilityMacros.h>
8 #include <mach/mach.h>
9 #include <mach/mach_time.h>
10 #include <stddef.h>
11 #include <stdint.h>
12 #include <sys/sysctl.h>
13 
14 #include <optional>
15 
16 #include "base/apple/mach_logging.h"
17 #include "base/apple/scoped_mach_port.h"
18 #include "base/containers/heap_array.h"
19 #include "base/logging.h"
20 #include "base/mac/mac_util.h"
21 #include "base/memory/ptr_util.h"
22 #include "base/notimplemented.h"
23 #include "base/numerics/safe_math.h"
24 #include "base/time/time.h"
25 #include "base/types/expected.h"
26 #include "build/build_config.h"
27 
28 #if BUILDFLAG(IS_MAC)
29 #include <libproc.h>
30 #include <mach/mach_vm.h>
31 #include <mach/shared_region.h>
32 #else
33 #include <mach/vm_region.h>
34 #if BUILDFLAG(USE_BLINK)
35 #include "base/ios/sim_header_shims.h"
36 #endif  // BUILDFLAG(USE_BLINK)
37 #endif
38 
39 namespace base {
40 
41 #define TIME_VALUE_TO_TIMEVAL(a, r)   \
42   do {                                \
43     (r)->tv_sec = (a)->seconds;       \
44     (r)->tv_usec = (a)->microseconds; \
45   } while (0)
46 
47 namespace {
48 
GetTaskInfo(mach_port_t task)49 base::expected<task_basic_info_64, ProcessCPUUsageError> GetTaskInfo(
50     mach_port_t task) {
51   if (task == MACH_PORT_NULL) {
52     return base::unexpected(ProcessCPUUsageError::kProcessNotFound);
53   }
54   task_basic_info_64 task_info_data{};
55   mach_msg_type_number_t count = TASK_BASIC_INFO_64_COUNT;
56   kern_return_t kr =
57       task_info(task, TASK_BASIC_INFO_64,
58                 reinterpret_cast<task_info_t>(&task_info_data), &count);
59   // Most likely cause for failure: |task| is a zombie.
60   if (kr != KERN_SUCCESS) {
61     return base::unexpected(ProcessCPUUsageError::kSystemError);
62   }
63   return base::ok(task_info_data);
64 }
65 
ParseOutputFromMachVMRegion(kern_return_t kr)66 MachVMRegionResult ParseOutputFromMachVMRegion(kern_return_t kr) {
67   if (kr == KERN_INVALID_ADDRESS) {
68     // We're at the end of the address space.
69     return MachVMRegionResult::Finished;
70   } else if (kr != KERN_SUCCESS) {
71     return MachVMRegionResult::Error;
72   }
73   return MachVMRegionResult::Success;
74 }
75 
GetPowerInfo(mach_port_t task,task_power_info * power_info_data)76 bool GetPowerInfo(mach_port_t task, task_power_info* power_info_data) {
77   if (task == MACH_PORT_NULL) {
78     return false;
79   }
80 
81   mach_msg_type_number_t power_info_count = TASK_POWER_INFO_COUNT;
82   kern_return_t kr = task_info(task, TASK_POWER_INFO,
83                                reinterpret_cast<task_info_t>(power_info_data),
84                                &power_info_count);
85   // Most likely cause for failure: |task| is a zombie.
86   return kr == KERN_SUCCESS;
87 }
88 
89 }  // namespace
90 
91 // Implementations of ProcessMetrics class shared by Mac and iOS.
TaskForHandle(ProcessHandle process_handle) const92 mach_port_t ProcessMetrics::TaskForHandle(ProcessHandle process_handle) const {
93   mach_port_t task = MACH_PORT_NULL;
94 #if BUILDFLAG(IS_MAC)
95   if (port_provider_) {
96     task = port_provider_->TaskForHandle(process_);
97   }
98 #endif
99   if (task == MACH_PORT_NULL && process_handle == getpid()) {
100     task = mach_task_self();
101   }
102   return task;
103 }
104 
105 base::expected<TimeDelta, ProcessCPUUsageError>
GetCumulativeCPUUsage()106 ProcessMetrics::GetCumulativeCPUUsage() {
107   mach_port_t task = TaskForHandle(process_);
108   if (task == MACH_PORT_NULL) {
109     return base::unexpected(ProcessCPUUsageError::kProcessNotFound);
110   }
111 
112   // Libtop explicitly loops over the threads (libtop_pinfo_update_cpu_usage()
113   // in libtop.c), but this is more concise and gives the same results:
114   task_thread_times_info thread_info_data;
115   mach_msg_type_number_t thread_info_count = TASK_THREAD_TIMES_INFO_COUNT;
116   kern_return_t kr = task_info(task, TASK_THREAD_TIMES_INFO,
117                                reinterpret_cast<task_info_t>(&thread_info_data),
118                                &thread_info_count);
119   if (kr != KERN_SUCCESS) {
120     // Most likely cause: |task| is a zombie.
121     return base::unexpected(ProcessCPUUsageError::kSystemError);
122   }
123 
124   const base::expected<task_basic_info_64, ProcessCPUUsageError>
125       task_info_data = GetTaskInfo(task);
126   if (!task_info_data.has_value()) {
127     return base::unexpected(task_info_data.error());
128   }
129 
130   /* Set total_time. */
131   // thread info contains live time...
132   struct timeval user_timeval, system_timeval, task_timeval;
133   TIME_VALUE_TO_TIMEVAL(&thread_info_data.user_time, &user_timeval);
134   TIME_VALUE_TO_TIMEVAL(&thread_info_data.system_time, &system_timeval);
135   timeradd(&user_timeval, &system_timeval, &task_timeval);
136 
137   // ... task info contains terminated time.
138   TIME_VALUE_TO_TIMEVAL(&task_info_data->user_time, &user_timeval);
139   TIME_VALUE_TO_TIMEVAL(&task_info_data->system_time, &system_timeval);
140   timeradd(&user_timeval, &task_timeval, &task_timeval);
141   timeradd(&system_timeval, &task_timeval, &task_timeval);
142 
143   const TimeDelta measured_cpu =
144       Microseconds(TimeValToMicroseconds(task_timeval));
145   if (measured_cpu < last_measured_cpu_) {
146     // When a thread terminates, its CPU time is immediately removed from the
147     // running thread times returned by TASK_THREAD_TIMES_INFO, but there can be
148     // a lag before it shows up in the terminated thread times returned by
149     // GetTaskInfo(). Make sure CPU usage doesn't appear to go backwards if
150     // GetCumulativeCPUUsage() is called in the interval.
151     return base::ok(last_measured_cpu_);
152   }
153   last_measured_cpu_ = measured_cpu;
154   return base::ok(measured_cpu);
155 }
156 
GetPackageIdleWakeupsPerSecond()157 int ProcessMetrics::GetPackageIdleWakeupsPerSecond() {
158   mach_port_t task = TaskForHandle(process_);
159   task_power_info power_info_data;
160 
161   GetPowerInfo(task, &power_info_data);
162 
163   // The task_power_info struct contains two wakeup counters:
164   // task_interrupt_wakeups and task_platform_idle_wakeups.
165   // task_interrupt_wakeups is the total number of wakeups generated by the
166   // process, and is the number that Activity Monitor reports.
167   // task_platform_idle_wakeups is a subset of task_interrupt_wakeups that
168   // tallies the number of times the processor was taken out of its low-power
169   // idle state to handle a wakeup. task_platform_idle_wakeups therefore result
170   // in a greater power increase than the other interrupts which occur while the
171   // CPU is already working, and reducing them has a greater overall impact on
172   // power usage. See the powermetrics man page for more info.
173   return CalculatePackageIdleWakeupsPerSecond(
174       power_info_data.task_platform_idle_wakeups);
175 }
176 
GetIdleWakeupsPerSecond()177 int ProcessMetrics::GetIdleWakeupsPerSecond() {
178   mach_port_t task = TaskForHandle(process_);
179   task_power_info power_info_data;
180 
181   GetPowerInfo(task, &power_info_data);
182 
183   return CalculateIdleWakeupsPerSecond(power_info_data.task_interrupt_wakeups);
184 }
185 
186 // Bytes committed by the system.
GetSystemCommitCharge()187 size_t GetSystemCommitCharge() {
188   base::apple::ScopedMachSendRight host(mach_host_self());
189   mach_msg_type_number_t count = HOST_VM_INFO_COUNT;
190   vm_statistics_data_t data;
191   kern_return_t kr = host_statistics(
192       host.get(), HOST_VM_INFO, reinterpret_cast<host_info_t>(&data), &count);
193   if (kr != KERN_SUCCESS) {
194     MACH_DLOG(WARNING, kr) << "host_statistics";
195     return 0;
196   }
197 
198   return (data.active_count * PAGE_SIZE) / 1024;
199 }
200 
GetSystemMemoryInfo(SystemMemoryInfoKB * meminfo)201 bool GetSystemMemoryInfo(SystemMemoryInfoKB* meminfo) {
202   struct host_basic_info hostinfo;
203   mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
204   base::apple::ScopedMachSendRight host(mach_host_self());
205   int result = host_info(host.get(), HOST_BASIC_INFO,
206                          reinterpret_cast<host_info_t>(&hostinfo), &count);
207   if (result != KERN_SUCCESS) {
208     return false;
209   }
210 
211   DCHECK_EQ(HOST_BASIC_INFO_COUNT, count);
212   meminfo->total = static_cast<int>(hostinfo.max_mem / 1024);
213 
214   vm_statistics64_data_t vm_info;
215   count = HOST_VM_INFO64_COUNT;
216 
217   if (host_statistics64(host.get(), HOST_VM_INFO64,
218                         reinterpret_cast<host_info64_t>(&vm_info),
219                         &count) != KERN_SUCCESS) {
220     return false;
221   }
222   DCHECK_EQ(HOST_VM_INFO64_COUNT, count);
223 
224 #if !(BUILDFLAG(IS_IOS) && defined(ARCH_CPU_X86_FAMILY))
225   // PAGE_SIZE (aka vm_page_size) isn't constexpr, so this check needs to be
226   // done at runtime.
227   DCHECK_EQ(PAGE_SIZE % 1024, 0u) << "Invalid page size";
228 #else
229   // On x86/x64, PAGE_SIZE used to be just a signed constant, I386_PGBYTES. When
230   // Arm Macs started shipping, PAGE_SIZE was defined from day one to be
231   // vm_page_size (an extern uintptr_t value), and the SDK, for x64, switched
232   // PAGE_SIZE to be vm_page_size for binaries targeted for macOS 11+:
233   //
234   // #if !defined(__MAC_OS_X_VERSION_MIN_REQUIRED) ||
235   //     (__MAC_OS_X_VERSION_MIN_REQUIRED < 101600)
236   //   #define PAGE_SIZE    I386_PGBYTES
237   // #else
238   //   #define PAGE_SIZE    vm_page_size
239   // #endif
240   //
241   // When building for Mac Catalyst or the iOS Simulator, this targeting
242   // switcharoo breaks. Because those apps do not have a
243   // __MAC_OS_X_VERSION_MIN_REQUIRED set, the SDK assumes that those apps are so
244   // old that they are implicitly targeting some ancient version of macOS, and a
245   // signed constant value is used for PAGE_SIZE.
246   //
247   // Therefore, when building for "iOS on x86", which is either Mac Catalyst or
248   // the iOS Simulator, use a static assert that assumes that PAGE_SIZE is a
249   // signed constant value.
250   //
251   // TODO(Chrome iOS team): Remove this entire #else branch when the Mac
252   // Catalyst and the iOS Simulator builds only target Arm Macs.
253   static_assert(PAGE_SIZE % 1024 == 0, "Invalid page size");
254 #endif  // !(defined(IS_IOS) && defined(ARCH_CPU_X86_FAMILY))
255 
256   if (vm_info.speculative_count <= vm_info.free_count) {
257     meminfo->free = saturated_cast<int>(
258         PAGE_SIZE / 1024 * (vm_info.free_count - vm_info.speculative_count));
259   } else {
260     // Inside the `host_statistics64` call above, `speculative_count` is
261     // computed later than `free_count`, so these values are snapshots of two
262     // (slightly) different points in time. As a result, it is possible for
263     // `speculative_count` to have increased significantly since `free_count`
264     // was computed, even to a point where `speculative_count` is greater than
265     // the computed value of `free_count`. See
266     // https://github.com/apple-oss-distributions/xnu/blob/aca3beaa3dfbd42498b42c5e5ce20a938e6554e5/osfmk/kern/host.c#L788
267     // In this case, 0 is the best approximation for `meminfo->free`. This is
268     // inexact, but even in the case where `speculative_count` is less than
269     // `free_count`, the computed `meminfo->free` will only be an approximation
270     // given that the two inputs come from different points in time.
271     meminfo->free = 0;
272   }
273 
274   meminfo->speculative =
275       saturated_cast<int>(PAGE_SIZE / 1024 * vm_info.speculative_count);
276   meminfo->file_backed =
277       saturated_cast<int>(PAGE_SIZE / 1024 * vm_info.external_page_count);
278   meminfo->purgeable =
279       saturated_cast<int>(PAGE_SIZE / 1024 * vm_info.purgeable_count);
280 
281   return true;
282 }
283 
284 // Both |size| and |address| are in-out parameters.
285 // |info| is an output parameter, only valid on Success.
GetTopInfo(mach_port_t task,mach_vm_size_t * size,mach_vm_address_t * address,vm_region_top_info_data_t * info)286 MachVMRegionResult GetTopInfo(mach_port_t task,
287                               mach_vm_size_t* size,
288                               mach_vm_address_t* address,
289                               vm_region_top_info_data_t* info) {
290   mach_msg_type_number_t info_count = VM_REGION_TOP_INFO_COUNT;
291   // The kernel always returns a null object for VM_REGION_TOP_INFO, but
292   // balance it with a deallocate in case this ever changes. See 10.9.2
293   // xnu-2422.90.20/osfmk/vm/vm_map.c vm_map_region.
294   apple::ScopedMachSendRight object_name;
295 
296   kern_return_t kr =
297 #if BUILDFLAG(IS_MAC)
298       mach_vm_region(task, address, size, VM_REGION_TOP_INFO,
299                      reinterpret_cast<vm_region_info_t>(info), &info_count,
300                      apple::ScopedMachSendRight::Receiver(object_name).get());
301 #else
302       vm_region_64(task, reinterpret_cast<vm_address_t*>(address),
303                    reinterpret_cast<vm_size_t*>(size), VM_REGION_TOP_INFO,
304                    reinterpret_cast<vm_region_info_t>(info), &info_count,
305                    apple::ScopedMachSendRight::Receiver(object_name).get());
306 #endif
307   return ParseOutputFromMachVMRegion(kr);
308 }
309 
GetBasicInfo(mach_port_t task,mach_vm_size_t * size,mach_vm_address_t * address,vm_region_basic_info_64 * info)310 MachVMRegionResult GetBasicInfo(mach_port_t task,
311                                 mach_vm_size_t* size,
312                                 mach_vm_address_t* address,
313                                 vm_region_basic_info_64* info) {
314   mach_msg_type_number_t info_count = VM_REGION_BASIC_INFO_COUNT_64;
315   // The kernel always returns a null object for VM_REGION_BASIC_INFO_64, but
316   // balance it with a deallocate in case this ever changes. See 10.9.2
317   // xnu-2422.90.20/osfmk/vm/vm_map.c vm_map_region.
318   apple::ScopedMachSendRight object_name;
319 
320   kern_return_t kr =
321 #if BUILDFLAG(IS_MAC)
322       mach_vm_region(task, address, size, VM_REGION_BASIC_INFO_64,
323                      reinterpret_cast<vm_region_info_t>(info), &info_count,
324                      apple::ScopedMachSendRight::Receiver(object_name).get());
325 
326 #else
327       vm_region_64(task, reinterpret_cast<vm_address_t*>(address),
328                    reinterpret_cast<vm_size_t*>(size), VM_REGION_BASIC_INFO_64,
329                    reinterpret_cast<vm_region_info_t>(info), &info_count,
330                    apple::ScopedMachSendRight::Receiver(object_name).get());
331 #endif
332   return ParseOutputFromMachVMRegion(kr);
333 }
334 
GetOpenFdCount() const335 int ProcessMetrics::GetOpenFdCount() const {
336 #if BUILDFLAG(USE_BLINK)
337   // In order to get a true count of the open number of FDs, PROC_PIDLISTFDS
338   // is used. This is done twice: first to get the appropriate size of a
339   // buffer, and then secondly to fill the buffer with the actual FD info.
340   //
341   // The buffer size returned in the first call is an estimate, based on the
342   // number of allocated fileproc structures in the kernel. This number can be
343   // greater than the actual number of open files, since the structures are
344   // allocated in slabs. The value returned in proc_bsdinfo::pbi_nfiles is
345   // also the number of allocated fileprocs, not the number in use.
346   //
347   // However, the buffer size returned in the second call is an accurate count
348   // of the open number of descriptors. The contents of the buffer are unused.
349   int rv = proc_pidinfo(process_, PROC_PIDLISTFDS, 0, nullptr, 0);
350   if (rv < 0) {
351     return -1;
352   }
353 
354   base::HeapArray<char> buffer =
355       base::HeapArray<char>::WithSize(static_cast<size_t>(rv));
356   rv = proc_pidinfo(process_, PROC_PIDLISTFDS, 0, buffer.data(), rv);
357   if (rv < 0) {
358     return -1;
359   }
360   return static_cast<int>(static_cast<unsigned long>(rv) / PROC_PIDLISTFD_SIZE);
361 #else
362   NOTIMPLEMENTED_LOG_ONCE();
363   return -1;
364 #endif  // BUILDFLAG(USE_BLINK)
365 }
366 
GetOpenFdSoftLimit() const367 int ProcessMetrics::GetOpenFdSoftLimit() const {
368   return checked_cast<int>(GetMaxFds());
369 }
370 
371 }  // namespace base
372