Commit 877d32ad authored by jeremy@chromium.org's avatar jeremy@chromium.org

[Telemetry] Improvements to OS X powermetrics integration

* Make StartMonitoringPowerAsync() synchronous in terms of starting the powermetrics process by waiting for output file to be created before returning.
* Handle parse errors in powermetrics output - empirically while I was able to quite easily get powermetrics into a state where it exited in the middle of writing a sample, I was completely unable to get it not write out at least one sample completely.
* Modify unit test to start/stop powermetrics faster than it's refresh rate to try to trigger malformed output - test fails without rest of this patch, passes with this patch.

BUG=338808

Review URL: https://codereview.chromium.org/145353012

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@247871 0039d316-1c4b-4281-b951-d872f2087c98
parent f8bbf6bb
...@@ -4,17 +4,21 @@ ...@@ -4,17 +4,21 @@
import collections import collections
import ctypes import ctypes
import logging
import os import os
import plistlib import plistlib
import shutil
import signal import signal
import tempfile import tempfile
import time import time
import xml.parsers.expat
try: try:
import resource # pylint: disable=F0401 import resource # pylint: disable=F0401
except ImportError: except ImportError:
resource = None # Not available on all platforms resource = None # Not available on all platforms
from ctypes import util from ctypes import util
from telemetry.core import util
from telemetry.core.platform import platform_backend from telemetry.core.platform import platform_backend
from telemetry.core.platform import posix_platform_backend from telemetry.core.platform import posix_platform_backend
...@@ -31,8 +35,9 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend): ...@@ -31,8 +35,9 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
class PowerMetricsUtility(object): class PowerMetricsUtility(object):
def __init__(self, backend): def __init__(self, backend):
self._powermetrics_process = None self._powermetrics_process = None
self._powermetrics_output_file = None
self._backend = backend self._backend = backend
self._output_filename = None
self._ouput_directory = None
@property @property
def binary_path(self): def binary_path(self):
...@@ -42,13 +47,28 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend): ...@@ -42,13 +47,28 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
assert not self._powermetrics_process, ( assert not self._powermetrics_process, (
"Must call StopMonitoringPowerAsync().") "Must call StopMonitoringPowerAsync().")
SAMPLE_INTERVAL_MS = 1000 / 20 # 20 Hz, arbitrary. SAMPLE_INTERVAL_MS = 1000 / 20 # 20 Hz, arbitrary.
self._powermetrics_output_file = tempfile.NamedTemporaryFile() # Empirically powermetrics creates an empty output file immediately upon
# starting. We detect file creation as a signal that measurement has
# started. In order to avoid various race conditions in tempfile creation
# we create a temp directory and have powermetrics create it's output
# there rather than say, creating a tempfile, deleting it and reusing its
# name.
self._ouput_directory = tempfile.mkdtemp()
self._output_filename = os.path.join(self._ouput_directory,
'powermetrics.output')
args = ['-f', 'plist', args = ['-f', 'plist',
'-i', '%d' % SAMPLE_INTERVAL_MS, '-i', '%d' % SAMPLE_INTERVAL_MS,
'-u', self._powermetrics_output_file.name] '-u', self._output_filename]
self._powermetrics_process = self._backend.LaunchApplication( self._powermetrics_process = self._backend.LaunchApplication(
self.binary_path, args, elevate_privilege=True) self.binary_path, args, elevate_privilege=True)
# Block until output file is written to ensure this function call is
# synchronous in respect to powermetrics starting.
def _OutputFileExists():
return os.path.isfile(self._output_filename)
timeout_sec = 2 * (SAMPLE_INTERVAL_MS / 1000.)
util.WaitFor(_OutputFileExists, timeout_sec)
def StopMonitoringPowerAsync(self): def StopMonitoringPowerAsync(self):
assert self._powermetrics_process, ( assert self._powermetrics_process, (
"StartMonitoringPowerAsync() not called.") "StartMonitoringPowerAsync() not called.")
...@@ -59,10 +79,12 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend): ...@@ -59,10 +79,12 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
returncode = self._powermetrics_process.wait() returncode = self._powermetrics_process.wait()
assert returncode in [0, -15], ( assert returncode in [0, -15], (
"powermetrics return code: %d" % returncode) "powermetrics return code: %d" % returncode)
return open(self._powermetrics_output_file.name, 'r').read()
return open(self._output_filename, 'rb').read()
finally: finally:
self._powermetrics_output_file.close() shutil.rmtree(self._ouput_directory)
self._powermetrics_output_file = None self._ouput_directory = None
self._output_filename = None
self._powermetrics_process = None self._powermetrics_process = None
def __init__(self): def __init__(self):
...@@ -187,6 +209,21 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend): ...@@ -187,6 +209,21 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
def StartMonitoringPowerAsync(self): def StartMonitoringPowerAsync(self):
self.powermetrics_tool_.StartMonitoringPowerAsync() self.powermetrics_tool_.StartMonitoringPowerAsync()
def _ParsePlistString(self, plist_string):
"""Wrapper to parse a plist from a string and catch any errors.
Sometimes powermetrics will exit in the middle of writing it's output,
empirically it seems that it always writes at least one sample in it's
entirety so we can safely ignore any errors in it's output.
Returns:
Parser output on succesful parse, None on parse error.
"""
try:
return plistlib.readPlistFromString(plist_string)
except xml.parsers.expat.ExpatError:
return None
def _ParsePowerMetricsOutput(self, powermetrics_output): def _ParsePowerMetricsOutput(self, powermetrics_output):
"""Parse output of powermetrics command line utility. """Parse output of powermetrics command line utility.
...@@ -238,7 +275,11 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend): ...@@ -238,7 +275,11 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
raw_plists = [x for x in raw_plists if len(x) > 0] raw_plists = [x for x in raw_plists if len(x) > 0]
# -------- Examine contents of first plist for systems specs. -------- # -------- Examine contents of first plist for systems specs. --------
plist = plistlib.readPlistFromString(raw_plists[0]) plist = self._ParsePlistString(raw_plists[0])
if not plist:
logging.warning("powermetrics produced invalid output, output length: "
"%d" % len(powermetrics_output))
return {}
if 'GPU' in plist: if 'GPU' in plist:
metrics.extend([ metrics.extend([
...@@ -274,7 +315,9 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend): ...@@ -274,7 +315,9 @@ class MacPlatformBackend(posix_platform_backend.PosixPlatformBackend):
# -------- Parse Data Out of Plists -------- # -------- Parse Data Out of Plists --------
for raw_plist in raw_plists: for raw_plist in raw_plists:
plist = plistlib.readPlistFromString(raw_plist) plist = self._ParsePlistString(raw_plist)
if not plist:
continue
# Duration of this sample. # Duration of this sample.
sample_duration_ms = int(plist['elapsed_ns']) / 10**6 sample_duration_ms = int(plist['elapsed_ns']) / 10**6
......
...@@ -16,5 +16,5 @@ class PlatformBackendTest(unittest.TestCase): ...@@ -16,5 +16,5 @@ class PlatformBackendTest(unittest.TestCase):
logging.warning('Test not supported on this platform.') logging.warning('Test not supported on this platform.')
return return
output = backend.MonitorPowerSync(100) output = backend.MonitorPowerSync(1)
self.assertTrue(output.has_key('power_samples_mw')) self.assertTrue(output.has_key('power_samples_mw'))
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment