chromium/content/test/gpu/measure_power_intel.py

# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This script runs power measurements for browsers using Intel Power Gadget.

This script only works on Windows/Mac with Intel CPU. Intel Power Gadget needs
to be installed on the machine before this script works. The software can be
downloaded from:
  https://software.intel.com/en-us/articles/intel-power-gadget

Newer IPG versions might also require Visual C++ 2010 runtime to be installed
on Windows:
  https://www.microsoft.com/en-us/download/details.aspx?id=14632

Install selenium via pip: `pip install selenium`
Selenium 4 is required for Edge. Selenium 4.00-alpha5 or later is recommended:
  `pip install selenium==4.0.0a5`

And finally install the web drivers for Chrome (and Edge if needed):
  http://chromedriver.chromium.org/downloads
  https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/

Sample runs:

python measure_power_intel.py --browser=canary --duration=10 --delay=5
  --verbose --url="https://www.youtube.com/watch?v=0XdS37Re1XQ"
  --extra-browser-args="--no-sandbox"

Supported browsers (--browser=xxx): 'stable', 'beta', 'dev', 'canary',
  'chromium', 'edge', and path_to_exe_file.
For Edge from insider channels (beta, dev, can), use path_to_exe_file.

It is recommended to test with optimized builds of Chromium e.g. these GN args:

  is_debug = false
  is_component_build = false
  is_official_build = true # optimization similar to official builds
  use_remoteexec = true
  enable_nacl = false
  proprietary_codecs = true
  ffmpeg_branding = "Chrome"

It might also help to disable unnecessary background services and to unplug the
power source some time before measuring.  See "Computer setup" section here:
  https://microsoftedge.github.io/videotest/2017-04/WebdriverMethodology.html
"""

import argparse
import csv
import datetime
import logging
import os
import shutil
import sys
import tempfile

try:
  from selenium import webdriver
  from selenium.common import exceptions
except ImportError as error:
  logging.error(
      'This script needs selenium and appropriate web drivers to be installed.')
  raise

import gpu_tests.ipg_utils as ipg_utils

CHROME_STABLE_PATH_WIN = (
    r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe')
CHROME_BETA_PATH_WIN = (
    r'C:\Program Files (x86)\Google\Chrome Beta\Application\chrome.exe')
CHROME_DEV_PATH_WIN = (
    r'C:\Program Files (x86)\Google\Chrome Dev\Application\chrome.exe')
# The following two paths are relative to the LOCALAPPDATA
CHROME_CANARY_PATH_WIN = r'Google\Chrome SxS\Application\chrome.exe'
CHROMIUM_PATH_WIN = r'Chromium\Application\chrome.exe'

CHROME_STABLE_PATH_MAC = (
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome')
CHROME_BETA_PATH_MAC = CHROME_STABLE_PATH_MAC
CHROME_DEV_PATH_MAC = CHROME_STABLE_PATH_MAC
CHROME_CANARY_PATH_MAC = (
    '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
)

SUPPORTED_BROWSERS = ['stable', 'beta', 'dev', 'canary', 'chromium', 'edge']


def LocateBrowserWin(options_browser):
  if options_browser == 'edge':
    return 'edge'
  browser = None
  if not options_browser or options_browser == 'stable':
    browser = CHROME_STABLE_PATH_WIN
  elif options_browser == 'beta':
    browser = CHROME_BETA_PATH_WIN
  elif options_browser == 'dev':
    browser = CHROME_DEV_PATH_WIN
  elif options_browser == 'canary':
    browser = os.path.join(os.getenv('LOCALAPPDATA'), CHROME_CANARY_PATH_WIN)
  elif options_browser == 'chromium':
    browser = os.path.join(os.getenv('LOCALAPPDATA'), CHROMIUM_PATH_WIN)
  elif options_browser.endswith('.exe'):
    browser = options_browser
  else:
    logging.warning('Invalid value for --browser')
    logging.warning(
        'Supported values: %s, or a full path to a browser executable.',
        ', '.join(SUPPORTED_BROWSERS))
    return None
  if not os.path.exists(browser):
    logging.warning("Can't locate browser at %s", browser)
    logging.warning('Please pass full path to the executable in --browser')
    return None
  return browser


def LocateBrowserMac(options_browser):
  browser = None
  if not options_browser or options_browser == 'stable':
    browser = CHROME_STABLE_PATH_MAC
  elif options_browser == 'beta':
    browser = CHROME_BETA_PATH_MAC
  elif options_browser == 'dev':
    browser = CHROME_DEV_PATH_MAC
  elif options_browser == 'canary':
    browser = CHROME_CANARY_PATH_MAC
  elif options_browser.endswith('Chromium'):
    browser = options_browser
  else:
    logging.warning('Invalid value for --browser')
    logging.warning(
        'Supported values: %s, or a full path to a browser executable.',
        ', '.join(SUPPORTED_BROWSERS))
    return None
  if not os.path.exists(browser):
    logging.warning("Can't locate browser at %s", browser)
    logging.warning('Please pass full path to the executable in --browser')
    return None
  return browser


def LocateBrowser(options_browser):
  if sys.platform == 'win32':
    return LocateBrowserWin(options_browser)
  if sys.platform == 'darwin':
    return LocateBrowserMac(options_browser)
  logging.warning('This script only runs on Windows/Mac.')
  return None


def CreateWebDriver(browser, user_data_dir, url, fullscreen,
                    extra_browser_args):
  if browser == 'edge' or browser.endswith('msedge.exe'):
    options = webdriver.EdgeOptions()
    # Set use_chromium to true or an error will be triggered that the latest
    # MSEdgeDriver doesn't support an older version (non-chrome based) of
    # MSEdge.
    options.use_chromium = True
    options.binary_location = browser
    for arg in extra_browser_args:
      options.add_argument(arg)
    logging.debug(' '.join(options.arguments))
    driver = webdriver.Edge(options=options)
  else:
    options = webdriver.ChromeOptions()
    options.binary_location = browser
    options.add_argument('--user-data-dir=%s' % user_data_dir)
    options.add_argument('--no-first-run')
    options.add_argument('--no-default-browser-check')
    options.add_argument('--autoplay-policy=no-user-gesture-required')
    options.add_argument('--start-maximized')
    for arg in extra_browser_args:
      options.add_argument(arg)
    logging.debug(' '.join(options.arguments))
    driver = webdriver.Chrome(options=options)
  driver.implicitly_wait(30)
  if url is not None:
    driver.get(url)
  if fullscreen:
    try:
      video_el = driver.find_element_by_tag_name('video')
      actions = webdriver.ActionChains(driver)
      actions.move_to_element(video_el)
      actions.double_click(video_el)
      actions.perform()
    except exceptions.InvalidSelectorException:
      logging.warning('Could not locate video element to make fullscreen')
  return driver


# pylint: disable=too-many-arguments
def MeasurePowerOnce(browser, logfile, duration, delay, resolution, url,
                     fullscreen, extra_browser_args):
  logging.debug('Logging into %s', logfile)
  user_data_dir = tempfile.mkdtemp()

  driver = CreateWebDriver(browser, user_data_dir, url, fullscreen,
                           extra_browser_args)
  ipg_utils.RunIPG(duration + delay, resolution, logfile)
  driver.quit()

  try:
    shutil.rmtree(user_data_dir)
  except Exception as err:  # pylint: disable=broad-except
    logging.warning('Failed to remove temporary folder: %s', user_data_dir)
    logging.warning('Please kill browser and remove it manually to avoid leak')
    logging.debug(err)
  results = ipg_utils.AnalyzeIPGLogFile(logfile, delay)
  return results
# pylint: enable=too-many-arguments


def ParseArgs():
  parser = argparse.ArgumentParser()
  parser.add_argument('--browser',
                      help=('select which browser to run. Options include: ' +
                            ', '.join(SUPPORTED_BROWSERS) +
                            ', or a full path to a browser executable. ' +
                            'By default, stable is selected.'))
  parser.add_argument('--duration',
                      default=60,
                      type=int,
                      help='specify how many seconds Intel Power Gadget '
                      'measures. By default, 60 seconds is selected.')
  parser.add_argument('--delay',
                      default=10,
                      type=int,
                      help='specify how many seconds we skip in the data '
                      'Intel Power Gadget collects. This time is for starting '
                      'video play, switching to fullscreen mode, etc. '
                      'By default, 10 seconds is selected.')
  parser.add_argument('--resolution',
                      default=100,
                      type=int,
                      help='specify how often Intel Power Gadget samples '
                      'data in milliseconds. By default, 100 ms is selected.')
  parser.add_argument('--logdir',
                      help='specify where Intel Power Gadget stores its log.'
                      'By default, it is the current path.')
  parser.add_argument('--logname',
                      help='specify the prefix for Intel Power Gadget log '
                      'filename. By default, it is PowerLog.')
  parser.add_argument('-v',
                      '--verbose',
                      action='store_true',
                      default=False,
                      help='print out debug information.')
  parser.add_argument('--repeat',
                      default=1,
                      type=int,
                      help='specify how many times to run the measurements.')
  parser.add_argument('--url',
                      help='specify the webpage URL the browser launches with.')
  parser.add_argument(
      '--extra-browser-args',
      dest='extra_browser_args',
      help='specify extra command line switches for the browser '
      'that are separated by spaces (quoted).')
  parser.add_argument(
      '--extra-browser-args-filename',
      dest='extra_browser_args_filename',
      metavar='FILE',
      help='specify extra command line switches for the browser '
      'in a text file that are separated by whitespace.')
  parser.add_argument('--fullscreen',
                      action='store_true',
                      default=False,
                      help='specify whether video should be made fullscreen.')

  return parser.parse_args()


def main():
  options = ParseArgs()
  if options.verbose:
    logging.basicConfig(level=logging.DEBUG)

  browser = LocateBrowser(options.browser)
  if not browser:
    return

  # TODO(zmo): Add code to disable a bunch of Windows services that might
  # affect power consumption.

  log_prefix = options.logname or 'PowerLog'

  all_results = []

  extra_browser_args = []
  if options.extra_browser_args:
    extra_browser_args = options.extra_browser_args.split()
  if options.extra_browser_args_filename:
    if not os.path.isfile(options.extra_browser_args_filename):
      logging.error("Can't locate file at %s",
                    options.extra_browser_args_filename)
    else:
      with open(options.extra_browser_args_filename, 'r') as f:
        extra_browser_args.extend(f.read().split())
        f.close()

  for run in range(1, options.repeat + 1):
    logfile = ipg_utils.GenerateIPGLogFilename(log_prefix, options.logdir, run,
                                               options.repeat, True)
    print('Iteration #%d out of %d' % (run, options.repeat))
    results = MeasurePowerOnce(browser, logfile, options.duration,
                               options.delay, options.resolution, options.url,
                               options.fullscreen, extra_browser_args)
    print(results)
    all_results.append(results)

  now = datetime.datetime.now()
  results_filename = '%s_%s_results.csv' % (log_prefix,
                                            now.strftime('%Y%m%d%H%M%S'))
  try:
    with open(results_filename, 'wb') as results_csv:
      labels = sorted(all_results[0].keys())
      w = csv.DictWriter(results_csv, fieldnames=labels)
      w.writeheader()
      w.writerows(all_results)
  except Exception as err:  # pylint: disable=broad-except
    logging.warning('Failed to write results file %s', results_filename)
    logging.debug(err)


if __name__ == '__main__':
  sys.exit(main())