b391e221bf68f33b9f02bbd13635da3e7915a9bc
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / platform / mac_platform_backend.py
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.
4
5 import collections
6 import ctypes
7 import logging
8 import os
9 import plistlib
10 import shutil
11 import signal
12 import tempfile
13 import time
14 import xml.parsers.expat
15 try:
16   import resource  # pylint: disable=F0401
17 except ImportError:
18   resource = None  # Not available on all platforms
19
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
24
25
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)
31
32
33 class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
34
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
41
42     @property
43     def binary_path(self):
44       return '/usr/bin/powermetrics'
45
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
55       # name.
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)
64
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)
71
72     def StopMonitoringPowerAsync(self):
73       assert self._powermetrics_process, (
74           "StartMonitoringPowerAsync() not called.")
75       # Tell powermetrics to take an immediate sample.
76       try:
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)
82
83         with open(self._output_filename, 'rb') as output_file:
84           return output_file.read()
85       finally:
86         shutil.rmtree(self._ouput_directory)
87         self._ouput_directory = None
88         self._output_filename = None
89         self._powermetrics_process = None
90
91   def __init__(self):
92     super(MacPlatformBackend, self).__init__()
93     self.libproc = None
94     self.powermetrics_tool_ = MacPlatformBackend.PowerMetricsUtility(self)
95
96   def StartRawDisplayFrameRateMeasurement(self):
97     raise NotImplementedError()
98
99   def StopRawDisplayFrameRateMeasurement(self):
100     raise NotImplementedError()
101
102   def GetRawDisplayFrameRateMeasurements(self):
103     raise NotImplementedError()
104
105   def IsThermallyThrottled(self):
106     raise NotImplementedError()
107
108   def HasBeenThermallyThrottled(self):
109     raise NotImplementedError()
110
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)]
133       PROC_PIDTASKINFO = 4
134       def __init__(self):
135         self.size = ctypes.sizeof(self)
136         super(ProcTaskInfo, self).__init__()
137
138     proc_info = ProcTaskInfo()
139     if not self.libproc:
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)
143
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}
148
149   def GetCpuTimestamp(self):
150     """Return current timestamp in seconds."""
151     return {'TotalTime': time.time()}
152
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
160     return 0
161
162   @decorators.Cache
163   def GetSystemTotalPhysicalMemory(self):
164     return int(self._RunCommand(['sysctl', '-n', 'hw.memsize']))
165
166   def PurgeUnpinnedMemory(self):
167     # TODO(pliard): Implement this.
168     pass
169
170   def GetMemoryStats(self, pid):
171     rss_vsz = self._GetPsOutput(['rss', 'vsz'], pid)
172     if rss_vsz:
173       rss, vsz = rss_vsz[0].split()
174       return {'VM': 1024 * int(vsz),
175               'WorkingSetSize': 1024 * int(rss)}
176     return {}
177
178   def GetOSName(self):
179     return 'mac'
180
181   @decorators.Cache
182   def GetOSVersionName(self):
183     os_version = os.uname()[2]
184
185     if os_version.startswith('9.'):
186       return LEOPARD
187     if os_version.startswith('10.'):
188       return SNOWLEOPARD
189     if os_version.startswith('11.'):
190       return LION
191     if os_version.startswith('12.'):
192       return MOUNTAINLION
193     if os_version.startswith('13.'):
194       return MAVERICKS
195
196     raise NotImplementedError('Unknown mac version %s.' % os_version)
197
198   def CanFlushIndividualFilesFromSystemCache(self):
199     return False
200
201   def FlushEntireSystemCache(self):
202     mavericks_or_later = self.GetOSVersionName() >= MAVERICKS
203     p = self.LaunchApplication('purge', elevate_privilege=mavericks_or_later)
204     p.wait()
205     assert p.returncode == 0, 'Failed to flush system cache'
206
207   @decorators.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)
212
213   def SetPowerMetricsUtilityForTest(self, obj):
214     self.powermetrics_tool_ = obj
215
216   def StartMonitoringPowerAsync(self):
217     self.powermetrics_tool_.StartMonitoringPowerAsync()
218
219   def _ParsePlistString(self, plist_string):
220     """Wrapper to parse a plist from a string and catch any errors.
221
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.
225
226     Returns:
227         Parser output on succesful parse, None on parse error.
228     """
229     try:
230       return plistlib.readPlistFromString(plist_string)
231     except xml.parsers.expat.ExpatError:
232       return None
233
234   def _ParsePowerMetricsOutput(self, powermetrics_output):
235     """Parse output of powermetrics command line utility.
236
237     Returns:
238         Dictionary in the format returned by StopMonitoringPowerAsync().
239     """
240
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, [])
249
250     # List of RunningAverage objects specifying metrics we want to aggregate.
251     metrics = [
252         ConstructMetric(
253             ['component_utilization', 'whole_package', 'average_frequency_mhz'],
254             ['processor','freq_hz']),
255         ConstructMetric(
256             ['component_utilization', 'whole_package', 'idle_percent'],
257             ['processor','packages', 0, 'c_state_ratio'])]
258
259     def DataWithMetricKeyPath(metric, powermetrics_output):
260       """Retrieve the sample from powermetrics' output for a given metric.
261
262       Args:
263           metric: The RunningAverage object we want to collect a new sample for.
264           powermetrics_output: Dictionary containing powermetrics output.
265
266       Returns:
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]
272
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)
276
277     power_samples = []
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]
283
284     # -------- Examine contents of first plist for systems specs. --------
285     plist = self._ParsePlistString(raw_plists[0])
286     if not plist:
287       logging.warning("powermetrics produced invalid output, output length: "
288           "%d" % len(powermetrics_output))
289       return {}
290
291     if 'GPU' in plist:
292       metrics.extend([
293           ConstructMetric(
294               ['component_utilization', 'gpu', 'average_frequency_mhz'],
295               ['GPU', 0, 'freq_hz']),
296           ConstructMetric(
297               ['component_utilization', 'gpu', 'idle_percent'],
298               ['GPU', 0, 'c_state_ratio'])])
299
300
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
303     # each one.
304     if 'processor' in plist:
305       core_dict = plist['processor']['packages'][0]['cores']
306       num_cores = len(core_dict)
307       cpu_num = 0
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']))
321           cpu_num += 1
322
323     # -------- Parse Data Out of Plists --------
324     for raw_plist in raw_plists:
325       plist = self._ParsePlistString(raw_plist)
326       if not plist:
327         continue
328
329       # Duration of this sample.
330       sample_duration_ms = int(plist['elapsed_ns']) / 10**6
331       sample_durations.append(sample_duration_ms)
332
333       if 'processor' not in plist:
334         continue
335       processor = plist['processor']
336
337       energy_consumption_mw = int(processor.get('package_watts', 0)) * 10**3
338
339       total_energy_consumption_mwh += (energy_consumption_mw *
340           (sample_duration_ms / 3600000.))
341
342       power_samples.append(energy_consumption_mw)
343
344       for m in metrics:
345         m.samples.append(DataWithMetricKeyPath(m, plist))
346
347     # -------- Collect and Process Data --------
348     out_dict = {}
349     out_dict['identifier'] = 'powermetrics'
350     # Raw power usage samples.
351     if power_samples:
352       out_dict['power_samples_mw'] = power_samples
353       out_dict['energy_consumption_mwh'] = total_energy_consumption_mwh
354
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.
358
359       Args:
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
364               metric.out_path.
365       """
366       if len(metric.samples) == 0:
367         return
368
369       assert len(metric.samples) == len(sample_durations)
370       avg = 0
371       for i in xrange(len(metric.samples)):
372         avg += metric.samples[i] * sample_durations[i]
373       avg /= sum(sample_durations)
374
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):
378           out[k] = {}
379         out = out[k]
380       out[metric.out_path[-1]] = avg
381
382     for m in metrics:
383       StoreMetricAverage(m, sample_durations, out_dict)
384     return out_dict
385
386   def StopMonitoringPowerAsync(self):
387     powermetrics_output = self.powermetrics_tool_.StopMonitoringPowerAsync()
388     assert len(powermetrics_output) > 0
389     return self._ParsePowerMetricsOutput(powermetrics_output)