1#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Dromaeo benchmark automation script.
7
8Script runs dromaeo tests in browsers specified by --browser switch and saves
9results to a spreadsheet on docs.google.com.
10
11Prerequisites:
121. Install Google Data APIs Python Client Library from
13   http://code.google.com/p/gdata-python-client.
142. Checkout Dromaeo benchmark from
15   http://src.chromium.org/svn/trunk/src/chrome/test/data/dromaeo and provide
16   local path to it in --dromaeo_home switch.
173. Create a spreadsheet at http://docs.google.com and specify its name in
18   --spreadsheet switch
19
20Benchmark results are presented in the following format:
21browser | date time
22test 1 name|m11|...|m1n|test 1 average mean| |e11|...|e1n|test 1 average error
23test 2 name|m21|...|m2n|test 2 average mean| |e21|...|e2n|test 2 average error
24...
25
26Here mij is mean run/s in individual dromaeo test i during benchmark run j,
27eij is error in individual dromaeo test i during benchmark run j.
28
29Example usage:
30dromaeo_benchmark_runner.py -b "E:\chromium\src\chrome\Release\chrome.exe"
31    -b "C:\Program Files (x86)\Safari\safari.exe"
32    -b "C:\Program Files (x86)\Opera 10.50 pre-alpha\opera.exe" -n 1
33    -d "E:\chromium\src\chrome\test\data\dromaeo" -f dom -e example@gmail.com
34
35"""
36
37import getpass
38import json
39import os
40import re
41import subprocess
42import time
43import urlparse
44from optparse import OptionParser
45from BaseHTTPServer import HTTPServer
46import SimpleHTTPServer
47import gdata.spreadsheet.service
48
49max_spreadsheet_columns = 20
50test_props = ['mean', 'error']
51
52
53def ParseArguments():
54  parser = OptionParser()
55  parser.add_option("-b", "--browser",
56                    action="append", dest="browsers",
57                    help="list of browsers to test")
58  parser.add_option("-n", "--run_count", dest="run_count", type="int",
59                    default=5, help="number of runs")
60  parser.add_option("-d", "--dromaeo_home", dest="dromaeo_home",
61                    help="directory with your dromaeo files")
62  parser.add_option("-p", "--port", dest="port", type="int",
63                    default=8080, help="http server port")
64  parser.add_option("-f", "--filter", dest="filter",
65                    default="dom", help="dromaeo suite filter")
66  parser.add_option("-e", "--email", dest="email",
67                    help="your google docs account")
68  parser.add_option("-s", "--spreadsheet", dest="spreadsheet_title",
69                    default="dromaeo",
70                    help="your google docs spreadsheet name")
71
72  options = parser.parse_args()[0]
73
74  if not options.dromaeo_home:
75    raise Exception('please specify dromaeo_home')
76
77  return options
78
79
80def KillProcessByName(process_name):
81  process = subprocess.Popen('wmic process get processid, executablepath',
82                             stdout=subprocess.PIPE)
83  stdout = str(process.communicate()[0])
84  match = re.search(re.escape(process_name) + '\s+(\d+)', stdout)
85  if match:
86    pid = match.group(1)
87    subprocess.call('taskkill /pid %s' % pid)
88
89
90class SpreadsheetWriter(object):
91  "Utility class for storing benchmarking results in Google spreadsheets."
92
93  def __init__(self, email, spreadsheet_title):
94    '''Login to google docs and search for spreadsheet'''
95
96    self.token_file = os.path.expanduser("~/.dromaeo_bot_auth_token")
97    self.gd_client = gdata.spreadsheet.service.SpreadsheetsService()
98
99    authenticated = False
100    if os.path.exists(self.token_file):
101      token = ''
102      try:
103        file = open(self.token_file, 'r')
104        token = file.read()
105        file.close()
106        self.gd_client.SetClientLoginToken(token)
107        self.gd_client.GetSpreadsheetsFeed()
108        authenticated = True
109      except (IOError, gdata.service.RequestError):
110        pass
111    if not authenticated:
112      self.gd_client.email = email
113      self.gd_client.password = getpass.getpass('Password for %s: ' % email)
114      self.gd_client.source = 'python robot for dromaeo'
115      self.gd_client.ProgrammaticLogin()
116      token = self.gd_client.GetClientLoginToken()
117      try:
118        file = open(self.token_file, 'w')
119        file.write(token)
120        file.close()
121      except (IOError):
122        pass
123      os.chmod(self.token_file, 0600)
124
125    # Search for the spreadsheet with title = spreadsheet_title.
126    spreadsheet_feed = self.gd_client.GetSpreadsheetsFeed()
127    for spreadsheet in spreadsheet_feed.entry:
128      if spreadsheet.title.text == spreadsheet_title:
129        self.spreadsheet_key = spreadsheet.id.text.rsplit('/', 1)[1]
130    if not self.spreadsheet_key:
131      raise Exception('Spreadsheet %s not found' % spreadsheet_title)
132
133    # Get the key of the first worksheet in spreadsheet.
134    worksheet_feed = self.gd_client.GetWorksheetsFeed(self.spreadsheet_key)
135    self.worksheet_key = worksheet_feed.entry[0].id.text.rsplit('/', 1)[1]
136
137  def _InsertRow(self, row):
138    row = dict([('c' + str(i), row[i]) for i in xrange(len(row))])
139    self.gd_client.InsertRow(row, self.spreadsheet_key, self.worksheet_key)
140
141  def _InsertBlankRow(self):
142    self._InsertRow('-' * self.columns_count)
143
144  def PrepareSpreadsheet(self, run_count):
145    """Update cells in worksheet topmost row with service information.
146
147    Calculate column count corresponding to run_count and create worksheet
148    column titles [c0, c1, ...] in the topmost row to speed up spreadsheet
149    updates (it allows to insert a whole row with a single request)
150    """
151
152    # Calculate the number of columns we need to present all test results.
153    self.columns_count = (run_count + 2) * len(test_props)
154    if self.columns_count > max_spreadsheet_columns:
155      # Google spreadsheet has just max_spreadsheet_columns columns.
156      max_run_count = max_spreadsheet_columns / len(test_props) - 2
157      raise Exception('maximum run count is %i' % max_run_count)
158    # Create worksheet column titles [c0, c1, ..., cn].
159    for i in xrange(self.columns_count):
160      self.gd_client.UpdateCell(1, i + 1, 'c' + str(i), self.spreadsheet_key,
161                                self.worksheet_key)
162
163  def WriteColumnTitles(self, run_count):
164    "Create titles for test results (mean 1, mean 2, ..., average mean, ...)"
165    row = []
166    for prop in test_props:
167      row.append('')
168      for i in xrange(run_count):
169        row.append('%s %i' % (prop, i + 1))
170      row.append('average ' + prop)
171    self._InsertRow(row)
172
173  def WriteBrowserBenchmarkTitle(self, browser_name):
174    "Create browser benchmark title (browser name, date time)"
175    self._InsertBlankRow()
176    self._InsertRow([browser_name, time.strftime('%d.%m.%Y %H:%M:%S')])
177
178  def WriteBrowserBenchmarkResults(self, test_name, test_data):
179    "Insert a row with single test results"
180    row = []
181    for prop in test_props:
182      if not row:
183        row.append(test_name)
184      else:
185        row.append('')
186      row.extend([str(x) for x in test_data[prop]])
187      row.append(str(sum(test_data[prop]) / len(test_data[prop])))
188    self._InsertRow(row)
189
190
191class DromaeoHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
192
193  def do_POST(self):
194    self.send_response(200)
195    self.end_headers()
196    self.wfile.write("<HTML>POST OK.<BR><BR>");
197    length = int(self.headers.getheader('content-length'))
198    parameters = urlparse.parse_qs(self.rfile.read(length))
199    self.server.got_post = True
200    self.server.post_data = parameters['data']
201
202
203class BenchmarkResults(object):
204  "Storage class for dromaeo benchmark results"
205
206  def __init__(self):
207    self.data = {}
208
209  def ProcessBrowserPostData(self, data):
210    "Convert dromaeo test results in internal format"
211    tests = json.loads(data[0])
212    for test in tests:
213      test_name = test['name']
214      if test_name not in self.data:
215        # Test is encountered for the first time.
216        self.data[test_name] = dict([(prop, []) for prop in test_props])
217      # Append current run results.
218      for prop in test_props:
219        value = -1
220        if prop in test: value = test[prop] # workaround for Opera 10.5
221        self.data[test_name][prop].append(value)
222
223
224def main():
225  options = ParseArguments()
226
227  # Start sever with dromaeo.
228  os.chdir(options.dromaeo_home)
229  server = HTTPServer(('', options.port), DromaeoHandler)
230
231  # Open and prepare spreadsheet on google docs.
232  spreadsheet_writer = SpreadsheetWriter(options.email,
233                                         options.spreadsheet_title)
234  spreadsheet_writer.PrepareSpreadsheet(options.run_count)
235  spreadsheet_writer.WriteColumnTitles(options.run_count)
236
237  for browser in options.browsers:
238    browser_name = os.path.splitext(os.path.basename(browser))[0]
239    spreadsheet_writer.WriteBrowserBenchmarkTitle(browser_name)
240    benchmark_results = BenchmarkResults()
241    for run_number in xrange(options.run_count):
242      print '%s run %i' % (browser_name, run_number + 1)
243      # Run browser.
244      test_page = 'http://localhost:%i/index.html?%s&automated&post_json' % (
245        options.port, options.filter)
246      browser_process = subprocess.Popen('%s "%s"' % (browser, test_page))
247      server.got_post = False
248      server.post_data = None
249      # Wait until POST request from browser.
250      while not server.got_post:
251        server.handle_request()
252      benchmark_results.ProcessBrowserPostData(server.post_data)
253      # Kill browser.
254      KillProcessByName(browser)
255      browser_process.wait()
256
257    # Insert test results into spreadsheet.
258    for (test_name, test_data) in benchmark_results.data.iteritems():
259      spreadsheet_writer.WriteBrowserBenchmarkResults(test_name, test_data)
260
261  server.socket.close()
262  return 0
263
264
265if __name__ == '__main__':
266  sys.exit(main())
267