1 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
14 import xml.parsers.expat
16 import resource # pylint: disable=F0401
18 resource = None # Not available on all platforms
20 from telemetry import decorators
21 from telemetry.core import util
22 from telemetry.core.platform import platform_backend
23 from telemetry.core.platform import posix_platform_backend
26 LEOPARD = platform_backend.OSVersion('leopard', 10.5)
27 SNOWLEOPARD = platform_backend.OSVersion('snowleopard', 10.6)
28 LION = platform_backend.OSVersion('lion', 10.7)
29 MOUNTAINLION = platform_backend.OSVersion('mountainlion', 10.8)
30 MAVERICKS = platform_backend.OSVersion('mavericks', 10.9)
33 class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
35 class PowerMetricsUtility(object):
36 def __init__(self, backend):
37 self._powermetrics_process = None
38 self._backend = backend
39 self._output_filename = None
40 self._ouput_directory = None
43 def binary_path(self):
44 return '/usr/bin/powermetrics'
46 def StartMonitoringPowerAsync(self):
47 assert not self._powermetrics_process, (
48 "Must call StopMonitoringPowerAsync().")
49 SAMPLE_INTERVAL_MS = 1000 / 20 # 20 Hz, arbitrary.
50 # Empirically powermetrics creates an empty output file immediately upon
51 # starting. We detect file creation as a signal that measurement has
52 # started. In order to avoid various race conditions in tempfile creation
53 # we create a temp directory and have powermetrics create it's output
54 # there rather than say, creating a tempfile, deleting it and reusing its
56 self._ouput_directory = tempfile.mkdtemp()
57 self._output_filename = os.path.join(self._ouput_directory,
58 'powermetrics.output')
59 args = ['-f', 'plist',
60 '-i', '%d' % SAMPLE_INTERVAL_MS,
61 '-u', self._output_filename]
62 self._powermetrics_process = self._backend.LaunchApplication(
63 self.binary_path, args, elevate_privilege=True)
65 # Block until output file is written to ensure this function call is
66 # synchronous in respect to powermetrics starting.
67 def _OutputFileExists():
68 return os.path.isfile(self._output_filename)
69 timeout_sec = 2 * (SAMPLE_INTERVAL_MS / 1000.)
70 util.WaitFor(_OutputFileExists, timeout_sec)
72 def StopMonitoringPowerAsync(self):
73 assert self._powermetrics_process, (
74 "StartMonitoringPowerAsync() not called.")
75 # Tell powermetrics to take an immediate sample.
77 self._powermetrics_process.send_signal(signal.SIGINFO)
78 self._powermetrics_process.send_signal(signal.SIGTERM)
79 returncode = self._powermetrics_process.wait()
80 assert returncode in [0, -15], (
81 "powermetrics return code: %d" % returncode)
83 with open(self._output_filename, 'rb') as output_file:
84 return output_file.read()
86 shutil.rmtree(self._ouput_directory)
87 self._ouput_directory = None
88 self._output_filename = None
89 self._powermetrics_process = None
92 super(MacPlatformBackend, self).__init__()
94 self.powermetrics_tool_ = MacPlatformBackend.PowerMetricsUtility(self)
96 def StartRawDisplayFrameRateMeasurement(self):
97 raise NotImplementedError()
99 def StopRawDisplayFrameRateMeasurement(self):
100 raise NotImplementedError()
102 def GetRawDisplayFrameRateMeasurements(self):
103 raise NotImplementedError()
105 def IsThermallyThrottled(self):
106 raise NotImplementedError()
108 def HasBeenThermallyThrottled(self):
109 raise NotImplementedError()
111 def GetCpuStats(self, pid):
112 """Return current cpu processing time of pid in seconds."""
113 class ProcTaskInfo(ctypes.Structure):
114 """Struct for proc_pidinfo() call."""
115 _fields_ = [("pti_virtual_size", ctypes.c_uint64),
116 ("pti_resident_size", ctypes.c_uint64),
117 ("pti_total_user", ctypes.c_uint64),
118 ("pti_total_system", ctypes.c_uint64),
119 ("pti_threads_user", ctypes.c_uint64),
120 ("pti_threads_system", ctypes.c_uint64),
121 ("pti_policy", ctypes.c_int32),
122 ("pti_faults", ctypes.c_int32),
123 ("pti_pageins", ctypes.c_int32),
124 ("pti_cow_faults", ctypes.c_int32),
125 ("pti_messages_sent", ctypes.c_int32),
126 ("pti_messages_received", ctypes.c_int32),
127 ("pti_syscalls_mach", ctypes.c_int32),
128 ("pti_syscalls_unix", ctypes.c_int32),
129 ("pti_csw", ctypes.c_int32),
130 ("pti_threadnum", ctypes.c_int32),
131 ("pti_numrunning", ctypes.c_int32),
132 ("pti_priority", ctypes.c_int32)]
135 self.size = ctypes.sizeof(self)
136 super(ProcTaskInfo, self).__init__()
138 proc_info = ProcTaskInfo()
140 self.libproc = ctypes.CDLL(ctypes.util.find_library('libproc'))
141 self.libproc.proc_pidinfo(pid, proc_info.PROC_PIDTASKINFO, 0,
142 ctypes.byref(proc_info), proc_info.size)
144 # Convert nanoseconds to seconds
145 cpu_time = (proc_info.pti_total_user / 1000000000.0 +
146 proc_info.pti_total_system / 1000000000.0)
147 return {'CpuProcessTime': cpu_time}
149 def GetCpuTimestamp(self):
150 """Return current timestamp in seconds."""
151 return {'TotalTime': time.time()}
153 def GetSystemCommitCharge(self):
154 vm_stat = self._RunCommand(['vm_stat'])
155 for stat in vm_stat.splitlines():
156 key, value = stat.split(':')
157 if key == 'Pages active':
158 pages_active = int(value.strip()[:-1]) # Strip trailing '.'
159 return pages_active * resource.getpagesize() / 1024
163 def GetSystemTotalPhysicalMemory(self):
164 return int(self._RunCommand(['sysctl', '-n', 'hw.memsize']))
166 def PurgeUnpinnedMemory(self):
167 # TODO(pliard): Implement this.
170 def GetMemoryStats(self, pid):
171 rss_vsz = self._GetPsOutput(['rss', 'vsz'], pid)
173 rss, vsz = rss_vsz[0].split()
174 return {'VM': 1024 * int(vsz),
175 'WorkingSetSize': 1024 * int(rss)}
182 def GetOSVersionName(self):
183 os_version = os.uname()[2]
185 if os_version.startswith('9.'):
187 if os_version.startswith('10.'):
189 if os_version.startswith('11.'):
191 if os_version.startswith('12.'):
193 if os_version.startswith('13.'):
196 raise NotImplementedError('Unknown mac version %s.' % os_version)
198 def CanFlushIndividualFilesFromSystemCache(self):
201 def FlushEntireSystemCache(self):
202 mavericks_or_later = self.GetOSVersionName() >= MAVERICKS
203 p = self.LaunchApplication('purge', elevate_privilege=mavericks_or_later)
205 assert p.returncode == 0, 'Failed to flush system cache'
208 def CanMonitorPowerAsync(self):
209 mavericks_or_later = self.GetOSVersionName() >= MAVERICKS
210 binary_path = self.powermetrics_tool_.binary_path
211 return mavericks_or_later and self.CanLaunchApplication(binary_path)
213 def SetPowerMetricsUtilityForTest(self, obj):
214 self.powermetrics_tool_ = obj
216 def StartMonitoringPowerAsync(self):
217 self.powermetrics_tool_.StartMonitoringPowerAsync()
219 def _ParsePlistString(self, plist_string):
220 """Wrapper to parse a plist from a string and catch any errors.
222 Sometimes powermetrics will exit in the middle of writing it's output,
223 empirically it seems that it always writes at least one sample in it's
224 entirety so we can safely ignore any errors in it's output.
227 Parser output on succesful parse, None on parse error.
230 return plistlib.readPlistFromString(plist_string)
231 except xml.parsers.expat.ExpatError:
234 def _ParsePowerMetricsOutput(self, powermetrics_output):
235 """Parse output of powermetrics command line utility.
238 Dictionary in the format returned by StopMonitoringPowerAsync().
241 # Container to collect samples for running averages.
242 # out_path - list containing the key path in the output dictionary.
243 # src_path - list containing the key path to get the data from in
244 # powermetrics' output.
245 def ConstructMetric(out_path, src_path):
246 RunningAverage = collections.namedtuple('RunningAverage', [
247 'out_path', 'src_path', 'samples'])
248 return RunningAverage(out_path, src_path, [])
250 # List of RunningAverage objects specifying metrics we want to aggregate.
253 ['component_utilization', 'whole_package', 'average_frequency_mhz'],
254 ['processor','freq_hz']),
256 ['component_utilization', 'whole_package', 'idle_percent'],
257 ['processor','packages', 0, 'c_state_ratio'])]
259 def DataWithMetricKeyPath(metric, powermetrics_output):
260 """Retrieve the sample from powermetrics' output for a given metric.
263 metric: The RunningAverage object we want to collect a new sample for.
264 powermetrics_output: Dictionary containing powermetrics output.
267 The sample corresponding to |metric|'s keypath."""
268 # Get actual data corresponding to key path.
269 out_data = powermetrics_output
270 for k in metric.src_path:
271 out_data = out_data[k]
273 assert type(out_data) in [int, float], (
274 "Was expecting a number: %s (%s)" % (type(out_data), out_data))
275 return float(out_data)
278 sample_durations = []
279 total_energy_consumption_mwh = 0
280 # powermetrics outputs multiple plists separated by null terminators.
281 raw_plists = powermetrics_output.split('\0')
282 raw_plists = [x for x in raw_plists if len(x) > 0]
284 # -------- Examine contents of first plist for systems specs. --------
285 plist = self._ParsePlistString(raw_plists[0])
287 logging.warning("powermetrics produced invalid output, output length: "
288 "%d" % len(powermetrics_output))
294 ['component_utilization', 'gpu', 'average_frequency_mhz'],
295 ['GPU', 0, 'freq_hz']),
297 ['component_utilization', 'gpu', 'idle_percent'],
298 ['GPU', 0, 'c_state_ratio'])])
301 # There's no way of knowing ahead of time how many cpus and packages the
302 # current system has. Iterate over cores and cpus - construct metrics for
304 if 'processor' in plist:
305 core_dict = plist['processor']['packages'][0]['cores']
306 num_cores = len(core_dict)
308 for core_idx in xrange(num_cores):
309 num_cpus = len(core_dict[core_idx]['cpus'])
310 base_src_path = ['processor', 'packages', 0, 'cores', core_idx]
311 for cpu_idx in xrange(num_cpus):
312 base_out_path = ['component_utilization', 'cpu%d' % cpu_num]
313 # C State ratio is per-package, component CPUs of that package may
314 # have different frequencies.
315 metrics.append(ConstructMetric(
316 base_out_path + ['average_frequency_mhz'],
317 base_src_path + ['cpus', cpu_idx, 'freq_hz']))
318 metrics.append(ConstructMetric(
319 base_out_path + ['idle_percent'],
320 base_src_path + ['c_state_ratio']))
323 # -------- Parse Data Out of Plists --------
324 for raw_plist in raw_plists:
325 plist = self._ParsePlistString(raw_plist)
329 # Duration of this sample.
330 sample_duration_ms = int(plist['elapsed_ns']) / 10**6
331 sample_durations.append(sample_duration_ms)
333 if 'processor' not in plist:
335 processor = plist['processor']
337 energy_consumption_mw = int(processor.get('package_watts', 0)) * 10**3
339 total_energy_consumption_mwh += (energy_consumption_mw *
340 (sample_duration_ms / 3600000.))
342 power_samples.append(energy_consumption_mw)
345 m.samples.append(DataWithMetricKeyPath(m, plist))
347 # -------- Collect and Process Data --------
349 out_dict['identifier'] = 'powermetrics'
350 # Raw power usage samples.
352 out_dict['power_samples_mw'] = power_samples
353 out_dict['energy_consumption_mwh'] = total_energy_consumption_mwh
355 def StoreMetricAverage(metric, sample_durations, out):
356 """Calculate average value of samples in a metric and store in output
357 path as specified by metric.
360 metric: A RunningAverage object containing samples to average.
361 sample_durations: A list which parallels the samples list containing
362 the time slice for each sample.
363 out: The output dicat, average is stored in the location specified by
366 if len(metric.samples) == 0:
369 assert len(metric.samples) == len(sample_durations)
371 for i in xrange(len(metric.samples)):
372 avg += metric.samples[i] * sample_durations[i]
373 avg /= sum(sample_durations)
375 # Store data in output, creating empty dictionaries as we go.
376 for k in metric.out_path[:-1]:
377 if not out.has_key(k):
380 out[metric.out_path[-1]] = avg
383 StoreMetricAverage(m, sample_durations, out_dict)
386 def StopMonitoringPowerAsync(self):
387 powermetrics_output = self.powermetrics_tool_.StopMonitoringPowerAsync()
388 assert len(powermetrics_output) > 0
389 return self._ParsePowerMetricsOutput(powermetrics_output)