<commit_before>#!/usr/bin/python

# Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

import datetime
import getpass
import math
try:
  from matplotlib.font_manager import FontProperties
  import matplotlib.pyplot as plt
except ImportError:
  pass # Only needed if we want to make graphs.
import optparse
import os
from os.path import dirname, abspath
import platform
import re
import shutil
import stat
import subprocess
import sys
import time
import traceback

TOOLS_PATH = os.path.join(dirname(dirname(dirname(abspath(__file__)))))
DART_INSTALL_LOCATION = abspath(os.path.join(dirname(abspath(__file__)),
                                             '..', '..', '..'))
sys.path.append(TOOLS_PATH)
import utils

"""This script runs to track performance and size progress of
different svn revisions. It tests to see if there a newer version of the code on
the server, and will sync and run the performance tests if so."""
class TestRunner(object):

  def __init__(self):
    self.verbose = False
    self.has_shell = False
    if platform.system() == 'Windows':
      # On Windows, shell must be true to get the correct environment variables.
      self.has_shell = True

  def run_cmd(self, cmd_list, outfile=None, append=False, std_in=''):
    """Run the specified command and print out any output to stdout.

    Args:
      cmd_list: a list of strings that make up the command to run
      outfile: a string indicating the name of the file that we should write
         stdout to
      append: True if we want to append to the file instead of overwriting it
      std_in: a string that should be written to the process executing to
         interact with it (if needed)"""
    if self.verbose:
      print ' '.join(cmd_list)
    out = subprocess.PIPE
    if outfile:
      mode = 'w'
      if append:
        mode = 'a'
      out = open(outfile, mode)
      if append:
        # Annoying Windows "feature" -- append doesn't actually append unless
        # you explicitly go to the end of the file.
        # http://mail.python.org/pipermail/python-list/2009-October/1221859.html
        out.seek(0, os.SEEK_END)
    p = subprocess.Popen(cmd_list, stdout = out, stderr=subprocess.PIPE,
                         stdin=subprocess.PIPE, shell=self.has_shell)
    output, _ = p.communicate(std_in);
    if output:
      print output
    return output

  def time_cmd(self, cmd):
    """Determine the amount of (real) time it takes to execute a given 
    command."""
    start = time.time()
    self.run_cmd(cmd)
    return time.time() - start

  @staticmethod
  def get_build_targets(suites):
    """Loop through a set of tests that we want to run and find the build
    targets that are necessary.
    
    Args:
      suites: The test suites that we wish to run."""
    build_targets = set()
    for test in suites:
      if test.build_targets is not None:
        for target in test.build_targets:
          build_targets.add(target)
    return build_targets

  def sync_and_build(self, suites):
    """Make sure we have the latest version of of the repo, and build it. We
    begin and end standing in DART_INSTALL_LOCATION.

    Args:
      suites: The set of suites that we wish to build.

    Returns:
      err_code = 1 if there was a problem building."""
    os.chdir(DART_INSTALL_LOCATION)

    self.run_cmd(['gclient', 'sync'])

    # On Windows, the output directory is marked as "Read Only," which causes an
    # error to be thrown when we use shutil.rmtree. This helper function changes
    # the permissions so we can still delete the directory.
    def on_rm_error(func, path, exc_info):
      if os.path.exists(path):
        os.chmod(path, stat.S_IWRITE)
        os.unlink(path)
    # TODO(efortuna): building the sdk locally is a band-aid until all build
    # platform SDKs are hosted in Google storage. Pull from https://sandbox.
    # google.com/storage/?arg=dart-dump-render-tree#dart-dump-render-tree%2Fsdk
    # eventually.
    # TODO(efortuna): Currently always building ia32 architecture because we 
    # don't have test statistics for what's passing on x64. Eliminate arch 
    # specification when we have tests running on x64, too.
    shutil.rmtree(os.path.join(os.getcwd(),
                  utils.GetBuildRoot(utils.GuessOS(), 'release', 'ia32')),
                  onerror=on_rm_error)

    for target in TestRunner.get_build_targets(suites):
      lines = self.run_cmd([os.path.join('.', 'tools', 'build.py'), '-m', 
                            'release', '--arch=ia32', target])

      for line in lines:
        if 'BUILD FAILED' in lines:
          # Someone checked in a broken build! Stop trying to make it work
          # and wait to try again.
          print 'Broken Build'
          return 1
    return 0

  def ensure_output_directory(self, dir_name):
    """Test that the listed directory name exists, and if not, create one for
    our output to be placed.

    Args:
      dir_name: the directory we will create if it does not exist."""
    dir_path = os.path.join(DART_INSTALL_LOCATION, 'tools', 
                            'testing', 'perf_testing', dir_name)
    if not os.path.exists(dir_path):
      os.mkdir(dir_path)
      print 'Creating output directory ', dir_path

  def has_new_code(self):
    """Tests if there are any newer versions of files on the server."""
    os.chdir(DART_INSTALL_LOCATION)
    # Pass 'p' in if we have a new certificate for the svn server, we want to
    # (p)ermanently accept it.
    results = self.run_cmd(['svn', 'st', '-u'], std_in='p')
    for line in results:
      if '*' in line:
        return True
    return False
    
  def get_os_directory(self):
    """Specifies the name of the directory for the testing build of dart, which
    has yet a different naming convention from utils.getBuildRoot(...)."""
    if platform.system() == 'Windows':
      return 'windows'
    elif platform.system() == 'Darwin':
      return 'macos'
    else:
      return 'linux'

  def upload_to_app_engine(self, suite_names):
    """Upload our results to our appengine server.
    Arguments:
      suite_names: Directories to upload data from (should match directory 
        names)."""
    # TODO(efortuna): This is the most basic way to get the data up
    # for others to view. Revisit this once we're serving nicer graphs (Google
    # Chart Tools) and from multiple perfbots and once we're in a position to
    # organize the data in a useful manner(!!).
    os.chdir(os.path.join(DART_INSTALL_LOCATION, 'tools', 'testing',
                          'perf_testing'))
    for data in suite_names:
      path = os.path.join('appengine', 'static', 'data', data, utils.GuessOS())
      shutil.rmtree(path, ignore_errors=True)
      os.makedirs(path)
      files = []
      # Copy the 1000 most recent trace files to be uploaded.
      for f in os.listdir(data):
        files += [(os.path.getmtime(os.path.join(data, f)), f)]
      files.sort()
      for f in files[-1000:]:
        shutil.copyfile(os.path.join(data, f[1]), 
                        os.path.join(path, f[1]+'.txt'))
    # Generate directory listing.
    for data in suite_names:
      path = os.path.join('appengine', 'static', 'data', data, utils.GuessOS())
      out = open(os.path.join('appengine', 'static',
                 '%s-%s.html' % (data, utils.GuessOS())), 'w')
      out.write('<html>\n  <body>\n    <ul>\n')
      for f in os.listdir(path):
        if not f.startswith('.'):
          out.write('      <li><a href=data' + \
                    '''/%(data)s/%(os)s/%(file)s>%(file)s</a></li>\n''' % \
                    {'data': data, 'os': utils.GuessOS(), 'file': f})
      out.write('    </ul>\n  </body>\n</html>')
      out.close()

    shutil.rmtree(os.path.join('appengine', 'static', 'graphs'),
                  ignore_errors=True)
    shutil.copytree('graphs', os.path.join('appengine', 'static', 'graphs'))
    shutil.copyfile('index.html', os.path.join('appengine', 'static',
                    'index.html'))
    shutil.copyfile('dromaeo.html', os.path.join('appengine', 'static',
                    'dromaeo.html'))
    shutil.copyfile('data.html', os.path.join('appengine', 'static',
                    'data.html'))
    self.run_cmd([os.path.join('..', '..', '..', 'third_party',
                               'appengine-python', 'appcfg.py'), '--oauth2', 
                               'update', 'appengine/'])

  def parse_args(self):
    parser = optparse.OptionParser()
    parser.add_option('--suites', '-s', dest='suites', help='Run the specified '
                      'comma-separated test suites from set: %s' % \
                      ','.join(TestBuilder.available_suite_names()), 
                      action='store', default=None)
    parser.add_option('--forever', '-f', dest='continuous', help='Run this scri'
                      'pt forever, always checking for the next svn checkin',
                      action='store_true', default=False)
    parser.add_option('--graph-only', '-g', dest='graph_only', default=False,
                      help='Do not run tests, only regenerate graphs', 
                      action='store_true')
    parser.add_option('--nobuild', '-n', dest='no_build', action='store_true',
                      help='Do not sync with the repository and do not '
                      'rebuild.', default=False)
    parser.add_option('--upload', '-u', dest='upload', help='Upload data to '
                      'app engine (will require authentication).', 
                      action='store_true', default=False)
    parser.add_option('--verbose', '-v', dest='verbose', help='Print extra '
                      'debug output', action='store_true', default=False)

    args, ignored = parser.parse_args()

    if not args.suites:
      suites = TestBuilder.available_suite_names()
    else:
      suites = []
      suitelist = args.suites.split(',')
      for name in suitelist:
        if name in TestBuilder.available_suite_names():
          suites.append(name)
        else:
          print ('Error: Invalid suite %s not in ' % name) + \
              '%s' % ','.join(TestBuilder.available_suite_names())
          sys.exit(1)
    self.suite_names = suites
    self.no_build = args.no_build
    self.graph_only = args.graph_only
    self.upload = args.upload
    self.verbose = args.verbose
    return args.continuous

  def run_test_sequence(self):
    """Run the set of commands to (possibly) build, run, and graph the results
    of our tests.

    Args:
      suite_names: The "display name" the user enters to specify which
          benchmark(s) to run.
      no_build: True if we should not check the repository and build the latest
          version.
      graph_only: True if we should not run the tests, just (re)generate graphs.
      upload: True if we should upload our results to appengine."""
    suites = []
    for name in self.suite_names:
      suites += [TestBuilder.make_test(name, self)]

    if not self.no_build and self.sync_and_build(suites) == 1:
      return # The build is broken.

    for test in suites:
      test.run(self.graph_only)

    if self.upload:
      self.upload_to_app_engine(TestBuilder.available_site_names())


class Test(object):
  """The base class to provide shared code for different tests we will run and
  graph. At a high level, each test has three visitors (the tester, the
  file_processor, and the grapher) that perform operations on the test 
  object."""

  def __init__(self, result_folder_name, platform_list, variants,
               values_list, test_runner, tester, file_processor, grapher,
               extra_metrics=['Geo-Mean'], build_targets=['create_sdk']):
    """Args:
         result_folder_name: The name of the folder where a tracefile of
             performance results will be stored.
         platform_list: A list containing the platform(s) that our data has been
             run on. (command line, firefox, chrome, etc)
         variants: A list specifying whether we hold data about Frog
             generated code, plain JS code, or a combination of both, or
             Dart depending on the test.
         values_list: A list containing the type of data we will be graphing
             (benchmarks, percentage passing, etc).
         test_runner: Reference to the parent test runner object that notifies a
             test when to run.
         tester: The visitor that actually performs the test running mechanics.
         file_processor: The visitor that processes files in the format
             appropriate for this test.
         grapher: The visitor that generates graphs given our test result data.
         extra_metrics: A list of any additional measurements we wish to keep
             track of (such as the geometric mean of a set, the sum, etc).
         build_targets: The targets necessary to build to run these tests
             (default target is create_sdk)."""
    self.result_folder_name = result_folder_name
    # cur_time is used as a timestamp of when this performance test was run.
    self.cur_time = str(time.mktime(datetime.datetime.now().timetuple()))
    self.values_list = values_list
    self.platform_list = platform_list
    self.revision_dict = dict()
    self.values_dict = dict()
    self.test_runner = test_runner
    self.tester = tester
    self.file_processor = file_processor
    self.grapher = grapher
    self.extra_metrics = extra_metrics
    self.build_targets = build_targets
    # Initialize our values store.
    for platform in platform_list:
      self.revision_dict[platform] = dict()
      self.values_dict[platform] = dict()
      for f in variants:
        self.revision_dict[platform][f] = dict()
        self.values_dict[platform][f] = dict()
        for val in values_list:
          self.revision_dict[platform][f][val] = []
          self.values_dict[platform][f][val] = []
        for extra_metric in extra_metrics:
          self.revision_dict[platform][f][extra_metric] = []
          self.values_dict[platform][f][extra_metric] = []
  
  def run(self, graph_only):
    """Run the benchmarks/tests from the command line and plot the
    results.
    
    Args:
      graph_only: True if we should just graph the results instead of also
          running tests."""
    for visitor in [self.tester, self.file_processor, self.grapher]:
      visitor.prepare()
    
    os.chdir(DART_INSTALL_LOCATION)
    self.test_runner.ensure_output_directory(self.result_folder_name)
    if not graph_only:
      self.tester.run_tests()

    os.chdir(os.path.join('tools', 'testing', 'perf_testing'))

    # TODO(efortuna): You will want to make this only use a subset of the files
    # eventually.
    files = os.listdir(self.result_folder_name)

    for afile in files:
      if not afile.startswith('.'):
        self.file_processor.process_file(afile)

    if 'plt' in globals():
      # Only run Matplotlib if it is installed.
      self.grapher.plot_results('%s.png' % self.result_folder_name)


class Tester(object):
  """The base level visitor class that runs tests. It contains convenience 
  methods that many Tester objects use. Any class that would like to be a
  TesterVisitor must implement the run_tests() method."""

  def __init__(self, test):
    self.test = test

  def prepare(self):
    """Perform any initial setup required before the test is run."""
    pass

  def add_svn_revision_to_trace(self, outfile):
    """Add the svn version number to the provided tracefile."""
    def search_for_revision(svn_info_command):
      p = subprocess.Popen(svn_info_command, stdout = subprocess.PIPE,
                           stderr = subprocess.STDOUT, shell =
                           self.test.test_runner.has_shell)
      output, _ = p.communicate()
      for line in output.split('\n'):
        if 'Revision' in line:
          self.test.test_runner.run_cmd(['echo', line.strip()], outfile)
          return True
      return False

    if not search_for_revision(['svn', 'info']):
      if not search_for_revision(['git', 'svn', 'info']):
        self.test.test_runner.run_cmd(['echo', 'Revision: unknown'], outfile)


class Processor(object):
  """The base level vistor class that processes tests. It contains convenience 
  methods that many File Processor objects use. Any class that would like to be
  a ProcessorVisitor must implement the process_file() method."""

  def __init__(self, test):
    self.test = test

  def prepare(self):
    """Perform any initial setup required before the test is run."""
    pass

  def calculate_geometric_mean(self, platform, variant, svn_revision):
    """Calculate the aggregate geometric mean for JS and frog benchmark sets,
    given two benchmark dictionaries."""
    geo_mean = 0
    for benchmark in self.test.values_list:
      geo_mean += math.log(self.test.values_dict[platform][variant][benchmark][
          len(self.test.values_dict[platform][variant][benchmark]) - 1])

    self.test.values_dict[platform][variant]['Geo-Mean'] += \
        [math.pow(math.e, geo_mean / len(self.test.values_list))]
    self.test.revision_dict[platform][variant]['Geo-Mean'] += [svn_revision]


class Grapher(object):
  """The base level visitor class that generates graphs for data. It contains 
  convenience methods that many Grapher objects use. Any class that would like
  to be a GrapherVisitor must implement the plot_results() method."""
  
  graph_out_dir = 'graphs'
  
  def __init__(self, test):
    self.color_index = 0
    self.test = test
  
  def prepare(self):
    """Perform any initial setup required before the test is run."""
    if 'plt' in globals():
      plt.cla() # cla = clear current axes
    else:
      print 'Unable to import Matplotlib and therefore unable to generate ' + \
          'graphs. Please install it for this version of Python.'
    self.test.test_runner.ensure_output_directory(Grapher.graph_out_dir)

  def style_and_save_perf_plot(self, chart_title, y_axis_label, size_x, size_y,
                               legend_loc, filename, platform_list, variants, 
                               values_list, should_clear_axes=True):
    """Sets style preferences for chart boilerplate that is consistent across
    all charts, and saves the chart as a png.

    Args:
      size_x: the size of the printed chart, in inches, in the horizontal
        direction
      size_y: the size of the printed chart, in inches in the vertical direction
      legend_loc: the location of the legend in on the chart. See suitable
        arguments for the loc argument in matplotlib
      filename: the filename that we want to save the resulting chart as
      platform_list: a list containing the platform(s) that our data has been
        run on. (command line, firefox, chrome, etc)
      values_list: a list containing the type of data we will be graphing
        (performance, percentage passing, etc)
      should_clear_axes: True if we want to create a fresh graph, instead of
        plotting additional lines on the current graph."""
    if should_clear_axes:
      plt.cla() # cla = clear current axes
    for platform in platform_list:
      for f in variants:
        for val in values_list:
          plt.plot(self.test.revision_dict[platform][f][val],
              self.test.values_dict[platform][f][val],
              color=self.get_color(), label='%s-%s-%s' % (platform, f, val))

    plt.xlabel('Revision Number')
    plt.ylabel(y_axis_label)
    plt.title(chart_title)
    fontP = FontProperties()
    fontP.set_size('small')
    plt.legend(loc=legend_loc, prop = fontP)

    fig = plt.gcf()
    fig.set_size_inches(size_x, size_y)
    fig.savefig(os.path.join(Grapher.graph_out_dir, filename))
  
  def get_color(self):
    # Just a bunch of distinct colors for a potentially large number of values
    # we wish to graph.
    colors = [
        'blue', 'green', 'red', 'cyan', 'magenta', 'black', '#3366CC',
        '#DC3912', '#FF9900', '#109618', '#990099', '#0099C6', '#DD4477',
        '#66AA00', '#B82E2E', '#316395', '#994499', '#22AA99', '#AAAA11',
        '#6633CC', '#E67300', '#8B0707', '#651067', '#329262', '#5574A6',
        '#3B3EAC', '#B77322', '#16D620', '#B91383', '#F4359E', '#9C5935',
        '#A9C413', '#2A778D', '#668D1C', '#BEA413', '#0C5922', '#743411',
        '#45AFE2', '#FF3300', '#FFCC00', '#14C21D', '#DF51FD', '#15CBFF',
        '#FF97D2', '#97FB00', '#DB6651', '#518BC6', '#BD6CBD', '#35D7C2',
        '#E9E91F', '#9877DD', '#FF8F20', '#D20B0B', '#B61DBA', '#40BD7E',
        '#6AA7C4', '#6D70CD', '#DA9136', '#2DEA36', '#E81EA6', '#F558AE',
        '#C07145', '#D7EE53', '#3EA7C6', '#97D129', '#E9CA1D', '#149638',
        '#C5571D']
    color = colors[self.color_index]
    self.color_index = (self.color_index + 1) % len(colors)
    return color


class RuntimePerformanceTest(Test):
  """Super class for all runtime performance testing."""

  def __init__(self, result_folder_name, platform_list, platform_type,
               versions, benchmarks, test_runner, tester, file_processor,
               build_targets=['create_sdk']):
    """Args:
        result_folder_name: The name of the folder where a tracefile of
            performance results will be stored.
        platform_list: A list containing the platform(s) that our data has been
            run on. (command line, firefox, chrome, etc)
        variants: A list specifying whether we hold data about Frog
            generated code, plain JS code, or a combination of both, or
            Dart depending on the test.
        values_list: A list containing the type of data we will be graphing
            (benchmarks, percentage passing, etc).
        test_runner: Reference to the parent test runner object that notifies a
            test when to run.
        tester: The visitor that actually performs the test running mechanics.
        file_processor: The visitor that processes files in the format
            appropriate for this test.
        grapher: The visitor that generates graphs given our test result data.
        extra_metrics: A list of any additional measurements we wish to keep
            track of (such as the geometric mean of a set, the sum, etc).
        build_targets: The targets necessary to build to run these tests
            (default target is create_sdk)."""
    super(RuntimePerformanceTest, self).__init__(result_folder_name,
          platform_list, versions, benchmarks, test_runner, tester,
          file_processor, self.RuntimePerfGrapher(self),
          build_targets=build_targets)
    self.platform_list = platform_list
    self.platform_type = platform_type
    self.versions = versions
    self.benchmarks = benchmarks 
 
  class RuntimePerfGrapher(Grapher):
    def plot_all_perf(self, png_filename):
      """Create a plot that shows the performance changes of individual
      benchmarks run by JS and generated by frog, over svn history."""
      for benchmark in self.test.benchmarks:
        self.style_and_save_perf_plot(
            'Performance of %s over time on the %s on %s' % (benchmark,
            self.test.platform_type, utils.GuessOS()), 
            'Speed (bigger = better)', 16, 14, 'lower left',
            benchmark + png_filename, self.test.platform_list,
            self.test.versions, [benchmark])

    def plot_avg_perf(self, png_filename):
      """Generate a plot that shows the performance changes of the geomentric
      mean of JS and frog benchmark performance over svn history."""
      (title, y_axis, size_x, size_y, loc, filename) = \
          ('Geometric Mean of benchmark %s performance on %s ' %
          (self.test.platform_type, utils.GuessOS()), 'Speed (bigger = better)',
          16, 5, 'lower left', 'avg'+png_filename)
      clear_axis = True
      for platform in self.test.platform_list:
        for version in self.test.versions:
          for metric in self.test.extra_metrics:
            self.style_and_save_perf_plot(title, y_axis, size_x, size_y, loc,
                                          filename, [platform], [version],
                                          [metric], clear_axis)

    def plot_results(self, png_filename):
      self.plot_all_perf(png_filename)
      self.plot_avg_perf('2' + png_filename)


class CommonCommandLineTest(RuntimePerformanceTest):
  """Run the basic performance tests (Benchpress, some V8 benchmarks) from the
  command line."""

  def __init__(self, test_runner):
    """Args:
      test_runner: Reference to the object that notfies this test when to<commit_msg>Fix graphing error. Review URL: https://chromiumcodereview.appspot.com//10069025<commit_after>#!/usr/bin/python

# Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

import datetime
import getpass
import math
try:
  from matplotlib.font_manager import FontProperties
  import matplotlib.pyplot as plt
except ImportError:
  pass # Only needed if we want to make graphs.
import optparse
import os
from os.path import dirname, abspath
import platform
import re
import shutil
import stat
import subprocess
import sys
import time
import traceback

TOOLS_PATH = os.path.join(dirname(dirname(dirname(abspath(__file__)))))
DART_INSTALL_LOCATION = abspath(os.path.join(dirname(abspath(__file__)),
                                             '..', '..', '..'))
sys.path.append(TOOLS_PATH)
import utils

"""This script runs to track performance and size progress of
different svn revisions. It tests to see if there a newer version of the code on
the server, and will sync and run the performance tests if so."""
class TestRunner(object):

  def __init__(self):
    self.verbose = False
    self.has_shell = False
    if platform.system() == 'Windows':
      # On Windows, shell must be true to get the correct environment variables.
      self.has_shell = True

  def run_cmd(self, cmd_list, outfile=None, append=False, std_in=''):
    """Run the specified command and print out any output to stdout.

    Args:
      cmd_list: a list of strings that make up the command to run
      outfile: a string indicating the name of the file that we should write
         stdout to
      append: True if we want to append to the file instead of overwriting it
      std_in: a string that should be written to the process executing to
         interact with it (if needed)"""
    if self.verbose:
      print ' '.join(cmd_list)
    out = subprocess.PIPE
    if outfile:
      mode = 'w'
      if append:
        mode = 'a'
      out = open(outfile, mode)
      if append:
        # Annoying Windows "feature" -- append doesn't actually append unless
        # you explicitly go to the end of the file.
        # http://mail.python.org/pipermail/python-list/2009-October/1221859.html
        out.seek(0, os.SEEK_END)
    p = subprocess.Popen(cmd_list, stdout = out, stderr=subprocess.PIPE,
                         stdin=subprocess.PIPE, shell=self.has_shell)
    output, _ = p.communicate(std_in);
    if output:
      print output
    return output

  def time_cmd(self, cmd):
    """Determine the amount of (real) time it takes to execute a given 
    command."""
    start = time.time()
    self.run_cmd(cmd)
    return time.time() - start

  @staticmethod
  def get_build_targets(suites):
    """Loop through a set of tests that we want to run and find the build
    targets that are necessary.
    
    Args:
      suites: The test suites that we wish to run."""
    build_targets = set()
    for test in suites:
      if test.build_targets is not None:
        for target in test.build_targets:
          build_targets.add(target)
    return build_targets

  def sync_and_build(self, suites):
    """Make sure we have the latest version of of the repo, and build it. We
    begin and end standing in DART_INSTALL_LOCATION.

    Args:
      suites: The set of suites that we wish to build.

    Returns:
      err_code = 1 if there was a problem building."""
    os.chdir(DART_INSTALL_LOCATION)

    self.run_cmd(['gclient', 'sync'])

    # On Windows, the output directory is marked as "Read Only," which causes an
    # error to be thrown when we use shutil.rmtree. This helper function changes
    # the permissions so we can still delete the directory.
    def on_rm_error(func, path, exc_info):
      if os.path.exists(path):
        os.chmod(path, stat.S_IWRITE)
        os.unlink(path)
    # TODO(efortuna): building the sdk locally is a band-aid until all build
    # platform SDKs are hosted in Google storage. Pull from https://sandbox.
    # google.com/storage/?arg=dart-dump-render-tree#dart-dump-render-tree%2Fsdk
    # eventually.
    # TODO(efortuna): Currently always building ia32 architecture because we 
    # don't have test statistics for what's passing on x64. Eliminate arch 
    # specification when we have tests running on x64, too.
    shutil.rmtree(os.path.join(os.getcwd(),
                  utils.GetBuildRoot(utils.GuessOS(), 'release', 'ia32')),
                  onerror=on_rm_error)

    for target in TestRunner.get_build_targets(suites):
      lines = self.run_cmd([os.path.join('.', 'tools', 'build.py'), '-m', 
                            'release', '--arch=ia32', target])

      for line in lines:
        if 'BUILD FAILED' in lines:
          # Someone checked in a broken build! Stop trying to make it work
          # and wait to try again.
          print 'Broken Build'
          return 1
    return 0

  def ensure_output_directory(self, dir_name):
    """Test that the listed directory name exists, and if not, create one for
    our output to be placed.

    Args:
      dir_name: the directory we will create if it does not exist."""
    dir_path = os.path.join(DART_INSTALL_LOCATION, 'tools', 
                            'testing', 'perf_testing', dir_name)
    if not os.path.exists(dir_path):
      os.mkdir(dir_path)
      print 'Creating output directory ', dir_path

  def has_new_code(self):
    """Tests if there are any newer versions of files on the server."""
    os.chdir(DART_INSTALL_LOCATION)
    # Pass 'p' in if we have a new certificate for the svn server, we want to
    # (p)ermanently accept it.
    results = self.run_cmd(['svn', 'st', '-u'], std_in='p')
    for line in results:
      if '*' in line:
        return True
    return False
    
  def get_os_directory(self):
    """Specifies the name of the directory for the testing build of dart, which
    has yet a different naming convention from utils.getBuildRoot(...)."""
    if platform.system() == 'Windows':
      return 'windows'
    elif platform.system() == 'Darwin':
      return 'macos'
    else:
      return 'linux'

  def upload_to_app_engine(self, suite_names):
    """Upload our results to our appengine server.
    Arguments:
      suite_names: Directories to upload data from (should match directory 
        names)."""
    # TODO(efortuna): This is the most basic way to get the data up
    # for others to view. Revisit this once we're serving nicer graphs (Google
    # Chart Tools) and from multiple perfbots and once we're in a position to
    # organize the data in a useful manner(!!).
    os.chdir(os.path.join(DART_INSTALL_LOCATION, 'tools', 'testing',
                          'perf_testing'))
    for data in suite_names:
      path = os.path.join('appengine', 'static', 'data', data, utils.GuessOS())
      shutil.rmtree(path, ignore_errors=True)
      os.makedirs(path)
      files = []
      # Copy the 1000 most recent trace files to be uploaded.
      for f in os.listdir(data):
        files += [(os.path.getmtime(os.path.join(data, f)), f)]
      files.sort()
      for f in files[-1000:]:
        shutil.copyfile(os.path.join(data, f[1]), 
                        os.path.join(path, f[1]+'.txt'))
    # Generate directory listing.
    for data in suite_names:
      path = os.path.join('appengine', 'static', 'data', data, utils.GuessOS())
      out = open(os.path.join('appengine', 'static',
                 '%s-%s.html' % (data, utils.GuessOS())), 'w')
      out.write('<html>\n  <body>\n    <ul>\n')
      for f in os.listdir(path):
        if not f.startswith('.'):
          out.write('      <li><a href=data' + \
                    '''/%(data)s/%(os)s/%(file)s>%(file)s</a></li>\n''' % \
                    {'data': data, 'os': utils.GuessOS(), 'file': f})
      out.write('    </ul>\n  </body>\n</html>')
      out.close()

    shutil.rmtree(os.path.join('appengine', 'static', 'graphs'),
                  ignore_errors=True)
    shutil.copytree('graphs', os.path.join('appengine', 'static', 'graphs'))
    shutil.copyfile('index.html', os.path.join('appengine', 'static',
                    'index.html'))
    shutil.copyfile('dromaeo.html', os.path.join('appengine', 'static',
                    'dromaeo.html'))
    shutil.copyfile('data.html', os.path.join('appengine', 'static',
                    'data.html'))
    self.run_cmd([os.path.join('..', '..', '..', 'third_party',
                               'appengine-python', 'appcfg.py'), '--oauth2', 
                               'update', 'appengine/'])

  def parse_args(self):
    parser = optparse.OptionParser()
    parser.add_option('--suites', '-s', dest='suites', help='Run the specified '
                      'comma-separated test suites from set: %s' % \
                      ','.join(TestBuilder.available_suite_names()), 
                      action='store', default=None)
    parser.add_option('--forever', '-f', dest='continuous', help='Run this scri'
                      'pt forever, always checking for the next svn checkin',
                      action='store_true', default=False)
    parser.add_option('--graph-only', '-g', dest='graph_only', default=False,
                      help='Do not run tests, only regenerate graphs', 
                      action='store_true')
    parser.add_option('--nobuild', '-n', dest='no_build', action='store_true',
                      help='Do not sync with the repository and do not '
                      'rebuild.', default=False)
    parser.add_option('--upload', '-u', dest='upload', help='Upload data to '
                      'app engine (will require authentication).', 
                      action='store_true', default=False)
    parser.add_option('--verbose', '-v', dest='verbose', help='Print extra '
                      'debug output', action='store_true', default=False)

    args, ignored = parser.parse_args()

    if not args.suites:
      suites = TestBuilder.available_suite_names()
    else:
      suites = []
      suitelist = args.suites.split(',')
      for name in suitelist:
        if name in TestBuilder.available_suite_names():
          suites.append(name)
        else:
          print ('Error: Invalid suite %s not in ' % name) + \
              '%s' % ','.join(TestBuilder.available_suite_names())
          sys.exit(1)
    self.suite_names = suites
    self.no_build = args.no_build
    self.graph_only = args.graph_only
    self.upload = args.upload
    self.verbose = args.verbose
    return args.continuous

  def run_test_sequence(self):
    """Run the set of commands to (possibly) build, run, and graph the results
    of our tests.

    Args:
      suite_names: The "display name" the user enters to specify which
          benchmark(s) to run.
      no_build: True if we should not check the repository and build the latest
          version.
      graph_only: True if we should not run the tests, just (re)generate graphs.
      upload: True if we should upload our results to appengine."""
    suites = []
    for name in self.suite_names:
      suites += [TestBuilder.make_test(name, self)]

    if not self.no_build and self.sync_and_build(suites) == 1:
      return # The build is broken.

    for test in suites:
      test.run(self.graph_only)

    if self.upload:
      self.upload_to_app_engine(TestBuilder.available_suite_names())


class Test(object):
  """The base class to provide shared code for different tests we will run and
  graph. At a high level, each test has three visitors (the tester, the
  file_processor, and the grapher) that perform operations on the test 
  object."""

  def __init__(self, result_folder_name, platform_list, variants,
               values_list, test_runner, tester, file_processor, grapher,
               extra_metrics=['Geo-Mean'], build_targets=['create_sdk']):
    """Args:
         result_folder_name: The name of the folder where a tracefile of
             performance results will be stored.
         platform_list: A list containing the platform(s) that our data has been
             run on. (command line, firefox, chrome, etc)
         variants: A list specifying whether we hold data about Frog
             generated code, plain JS code, or a combination of both, or
             Dart depending on the test.
         values_list: A list containing the type of data we will be graphing
             (benchmarks, percentage passing, etc).
         test_runner: Reference to the parent test runner object that notifies a
             test when to run.
         tester: The visitor that actually performs the test running mechanics.
         file_processor: The visitor that processes files in the format
             appropriate for this test.
         grapher: The visitor that generates graphs given our test result data.
         extra_metrics: A list of any additional measurements we wish to keep
             track of (such as the geometric mean of a set, the sum, etc).
         build_targets: The targets necessary to build to run these tests
             (default target is create_sdk)."""
    self.result_folder_name = result_folder_name
    # cur_time is used as a timestamp of when this performance test was run.
    self.cur_time = str(time.mktime(datetime.datetime.now().timetuple()))
    self.values_list = values_list
    self.platform_list = platform_list
    self.revision_dict = dict()
    self.values_dict = dict()
    self.test_runner = test_runner
    self.tester = tester
    self.file_processor = file_processor
    self.grapher = grapher
    self.extra_metrics = extra_metrics
    self.build_targets = build_targets
    # Initialize our values store.
    for platform in platform_list:
      self.revision_dict[platform] = dict()
      self.values_dict[platform] = dict()
      for f in variants:
        self.revision_dict[platform][f] = dict()
        self.values_dict[platform][f] = dict()
        for val in values_list:
          self.revision_dict[platform][f][val] = []
          self.values_dict[platform][f][val] = []
        for extra_metric in extra_metrics:
          self.revision_dict[platform][f][extra_metric] = []
          self.values_dict[platform][f][extra_metric] = []
  
  def run(self, graph_only):
    """Run the benchmarks/tests from the command line and plot the
    results.
    
    Args:
      graph_only: True if we should just graph the results instead of also
          running tests."""
    for visitor in [self.tester, self.file_processor, self.grapher]:
      visitor.prepare()
    
    os.chdir(DART_INSTALL_LOCATION)
    self.test_runner.ensure_output_directory(self.result_folder_name)
    if not graph_only:
      self.tester.run_tests()

    os.chdir(os.path.join('tools', 'testing', 'perf_testing'))

    # TODO(efortuna): You will want to make this only use a subset of the files
    # eventually.
    files = os.listdir(self.result_folder_name)

    for afile in files:
      if not afile.startswith('.'):
        self.file_processor.process_file(afile)

    if 'plt' in globals():
      # Only run Matplotlib if it is installed.
      self.grapher.plot_results('%s.png' % self.result_folder_name)


class Tester(object):
  """The base level visitor class that runs tests. It contains convenience 
  methods that many Tester objects use. Any class that would like to be a
  TesterVisitor must implement the run_tests() method."""

  def __init__(self, test):
    self.test = test

  def prepare(self):
    """Perform any initial setup required before the test is run."""
    pass

  def add_svn_revision_to_trace(self, outfile):
    """Add the svn version number to the provided tracefile."""
    def search_for_revision(svn_info_command):
      p = subprocess.Popen(svn_info_command, stdout = subprocess.PIPE,
                           stderr = subprocess.STDOUT, shell =
                           self.test.test_runner.has_shell)
      output, _ = p.communicate()
      for line in output.split('\n'):
        if 'Revision' in line:
          self.test.test_runner.run_cmd(['echo', line.strip()], outfile)
          return True
      return False

    if not search_for_revision(['svn', 'info']):
      if not search_for_revision(['git', 'svn', 'info']):
        self.test.test_runner.run_cmd(['echo', 'Revision: unknown'], outfile)


class Processor(object):
  """The base level vistor class that processes tests. It contains convenience 
  methods that many File Processor objects use. Any class that would like to be
  a ProcessorVisitor must implement the process_file() method."""

  def __init__(self, test):
    self.test = test

  def prepare(self):
    """Perform any initial setup required before the test is run."""
    pass

  def calculate_geometric_mean(self, platform, variant, svn_revision):
    """Calculate the aggregate geometric mean for JS and frog benchmark sets,
    given two benchmark dictionaries."""
    geo_mean = 0
    for benchmark in self.test.values_list:
      geo_mean += math.log(self.test.values_dict[platform][variant][benchmark][
          len(self.test.values_dict[platform][variant][benchmark]) - 1])

    self.test.values_dict[platform][variant]['Geo-Mean'] += \
        [math.pow(math.e, geo_mean / len(self.test.values_list))]
    self.test.revision_dict[platform][variant]['Geo-Mean'] += [svn_revision]


class Grapher(object):
  """The base level visitor class that generates graphs for data. It contains 
  convenience methods that many Grapher objects use. Any class that would like
  to be a GrapherVisitor must implement the plot_results() method."""
  
  graph_out_dir = 'graphs'
  
  def __init__(self, test):
    self.color_index = 0
    self.test = test
  
  def prepare(self):
    """Perform any initial setup required before the test is run."""
    if 'plt' in globals():
      plt.cla() # cla = clear current axes
    else:
      print 'Unable to import Matplotlib and therefore unable to generate ' + \
          'graphs. Please install it for this version of Python.'
    self.test.test_runner.ensure_output_directory(Grapher.graph_out_dir)

  def style_and_save_perf_plot(self, chart_title, y_axis_label, size_x, size_y,
                               legend_loc, filename, platform_list, variants, 
                               values_list, should_clear_axes=True):
    """Sets style preferences for chart boilerplate that is consistent across
    all charts, and saves the chart as a png.

    Args:
      size_x: the size of the printed chart, in inches, in the horizontal
        direction
      size_y: the size of the printed chart, in inches in the vertical direction
      legend_loc: the location of the legend in on the chart. See suitable
        arguments for the loc argument in matplotlib
      filename: the filename that we want to save the resulting chart as
      platform_list: a list containing the platform(s) that our data has been
        run on. (command line, firefox, chrome, etc)
      values_list: a list containing the type of data we will be graphing
        (performance, percentage passing, etc)
      should_clear_axes: True if we want to create a fresh graph, instead of
        plotting additional lines on the current graph."""
    if should_clear_axes:
      plt.cla() # cla = clear current axes
    for platform in platform_list:
      for f in variants:
        for val in values_list:
          plt.plot(self.test.revision_dict[platform][f][val],
              self.test.values_dict[platform][f][val],
              color=self.get_color(), label='%s-%s-%s' % (platform, f, val))

    plt.xlabel('Revision Number')
    plt.ylabel(y_axis_label)
    plt.title(chart_title)
    fontP = FontProperties()
    fontP.set_size('small')
    plt.legend(loc=legend_loc, prop = fontP)

    fig = plt.gcf()
    fig.set_size_inches(size_x, size_y)
    fig.savefig(os.path.join(Grapher.graph_out_dir, filename))
  
  def get_color(self):
    # Just a bunch of distinct colors for a potentially large number of values
    # we wish to graph.
    colors = [
        'blue', 'green', 'red', 'cyan', 'magenta', 'black', '#3366CC',
        '#DC3912', '#FF9900', '#109618', '#990099', '#0099C6', '#DD4477',
        '#66AA00', '#B82E2E', '#316395', '#994499', '#22AA99', '#AAAA11',
        '#6633CC', '#E67300', '#8B0707', '#651067', '#329262', '#5574A6',
        '#3B3EAC', '#B77322', '#16D620', '#B91383', '#F4359E', '#9C5935',
        '#A9C413', '#2A778D', '#668D1C', '#BEA413', '#0C5922', '#743411',
        '#45AFE2', '#FF3300', '#FFCC00', '#14C21D', '#DF51FD', '#15CBFF',
        '#FF97D2', '#97FB00', '#DB6651', '#518BC6', '#BD6CBD', '#35D7C2',
        '#E9E91F', '#9877DD', '#FF8F20', '#D20B0B', '#B61DBA', '#40BD7E',
        '#6AA7C4', '#6D70CD', '#DA9136', '#2DEA36', '#E81EA6', '#F558AE',
        '#C07145', '#D7EE53', '#3EA7C6', '#97D129', '#E9CA1D', '#149638',
        '#C5571D']
    color = colors[self.color_index]
    self.color_index = (self.color_index + 1) % len(colors)
    return color


class RuntimePerformanceTest(Test):
  """Super class for all runtime performance testing."""

  def __init__(self, result_folder_name, platform_list, platform_type,
               versions, benchmarks, test_runner, tester, file_processor,
               build_targets=['create_sdk']):
    """Args:
        result_folder_name: The name of the folder where a tracefile of
            performance results will be stored.
        platform_list: A list containing the platform(s) that our data has been
            run on. (command line, firefox, chrome, etc)
        variants: A list specifying whether we hold data about Frog
            generated code, plain JS code, or a combination of both, or
            Dart depending on the test.
        values_list: A list containing the type of data we will be graphing
            (benchmarks, percentage passing, etc).
        test_runner: Reference to the parent test runner object that notifies a
            test when to run.
        tester: The visitor that actually performs the test running mechanics.
        file_processor: The visitor that processes files in the format
            appropriate for this test.
        grapher: The visitor that generates graphs given our test result data.
        extra_metrics: A list of any additional measurements we wish to keep
            track of (such as the geometric mean of a set, the sum, etc).
        build_targets: The targets necessary to build to run these tests
            (default target is create_sdk)."""
    super(RuntimePerformanceTest, self).__init__(result_folder_name,
          platform_list, versions, benchmarks, test_runner, tester,
          file_processor, self.RuntimePerfGrapher(self),
          build_targets=build_targets)
    self.platform_list = platform_list
    self.platform_type = platform_type
    self.versions = versions
    self.benchmarks = benchmarks 
 
  class RuntimePerfGrapher(Grapher):
    def plot_all_perf(self, png_filename):
      """Create a plot that shows the performance changes of individual
      benchmarks run by JS and generated by frog, over svn history."""
      for benchmark in self.test.benchmarks:
        self.style_and_save_perf_plot(
            'Performance of %s over time on the %s on %s' % (benchmark,
            self.test.platform_type, utils.GuessOS()), 
            'Speed (bigger = better)', 16, 14, 'lower left',
            benchmark + png_filename, self.test.platform_list,
            self.test.versions, [benchmark])

    def plot_avg_perf(self, png_filename):
      """Generate a plot that shows the performance changes of the geomentric
      mean of JS and frog benchmark performance over svn history."""
      (title, y_axis, size_x, size_y, loc, filename) = \
          ('Geometric Mean of benchmark %s performance on %s ' %
          (self.test.platform_type, utils.GuessOS()), 'Speed (bigger = better)',
          16, 5, 'lower left', 'avg'+png_filename)
      clear_axis = True
      for platform in self.test.platform_list:
        for version in self.test.versions:
          for metric in self.test.extra_metrics:
            self.style_and_save_perf_plot(title, y_axis, size_x, size_y, loc,
                                          filename, [platform], [version],
                                          [metric], clear_axis)
            clear_axis = False

    def plot_results(self, png_filename):
      self.plot_all_perf(png_filename)
      self.plot_avg_perf('2' + png_filename)


class CommonCommandLineTest(RuntimePerformanceTest):
  """Run the basic performance tests (Benchpress, some V8 benchmarks) from the
  command line."""

  def __init__(self, test_runner):
    """Args:
      test_runner: Reference to the object that notfies this test when to
{'fraction_non_alphanumeric': 0.1048777033725141, 'fraction_numerical': 0.012260009306277691, 'mean_word_length': 2.7198976520168574, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 23, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3621766', 'n_tokens_mistral': 14945, 'n_tokens_neox': 14231, 'n_words': 4766}