1#!/usr/bin/env python
2# Copyright (c) 2013 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be found
4# in the LICENSE file.
5
6""" Analyze per-tile and viewport bench data, and output visualized results.
7"""
8
9__author__ = 'bensong@google.com (Ben Chen)'
10
11import bench_util
12import boto
13import math
14import optparse
15import os
16import re
17import shutil
18
19from oauth2_plugin import oauth2_plugin
20
21# The default platform to analyze. Used when OPTION_PLATFORM flag is not set.
22DEFAULT_PLATFORM = 'Nexus10_4-1_Float_Bench_32'
23
24# Template for gsutil uri.
25GOOGLE_STORAGE_URI_SCHEME = 'gs'
26URI_BUCKET = 'chromium-skia-gm'
27
28# Maximum number of rows of tiles to track for viewport covering.
29MAX_TILE_ROWS = 8
30
31# Constants for optparse.
32USAGE_STRING = 'USAGE: %s [options]'
33HOWTO_STRING = """
34Note: to read bench data stored in Google Storage, you will need to set up the
35corresponding Python library.
36See http://developers.google.com/storage/docs/gspythonlibrary for details.
37"""
38HELP_STRING = """
39For the given platform and revision number, find corresponding viewport and
40tile benchmarks for each available picture bench, and output visualization and
41analysis in HTML. By default it reads from Skia's Google Storage location where
42bot data are stored, but if --dir is given, will read from local directory
43instead.
44""" + HOWTO_STRING
45
46OPTION_DIR = '--dir'
47OPTION_DIR_SHORT = '-d'
48OPTION_REVISION = '--rev'
49OPTION_REVISION_SHORT = '-r'
50OPTION_PLATFORM = '--platform'
51OPTION_PLATFORM_SHORT = '-p'
52# Bench representation algorithm flag.
53OPTION_REPRESENTATION_ALG = '--algorithm'
54OPTION_REPRESENTATION_ALG_SHORT = '-a'
55
56# Bench representation algorithm. See trunk/bench/bench_util.py.
57REPRESENTATION_ALG = bench_util.ALGORITHM_25TH_PERCENTILE
58
59# Constants for bench file matching.
60GOOGLE_STORAGE_OBJECT_NAME_PREFIX = 'perfdata/Skia_'
61BENCH_FILE_PREFIX_TEMPLATE = 'bench_r%s_'
62TILING_FILE_NAME_INDICATOR = '_tile_'
63VIEWPORT_FILE_NAME_INDICATOR = '_viewport_'
64
65# Regular expression for matching format '<integer>x<integer>'.
66DIMENSIONS_RE = '(\d+)x(\d+)'
67
68# HTML and JS output templates.
69HTML_PREFIX = """
70<html><head><script type="text/javascript" src="https://www.google.com/jsapi">
71</script><script type="text/javascript">google.load("visualization", "1.1",
72{packages:["table"]});google.load("prototype", "1.6");</script>
73<script type="text/javascript" src="https://systemsbiology-visualizations.googlecode.com/svn/trunk/src/main/js/load.js"></script><script
74type="text/javascript"> systemsbiology.load("visualization", "1.0",
75{packages:["bioheatmap"]});</script><script type="text/javascript">
76google.setOnLoadCallback(drawVisualization); function drawVisualization() {
77"""
78HTML_SUFFIX = '</body></html>'
79BAR_CHART_TEMPLATE = ('<img src="https://chart.googleapis.com/chart?chxr=0,0,'
80    '300&chxt=x&chbh=15,0&chs=600x150&cht=bhg&chco=80C65A,224499,FF0000,0A8C8A,'
81    'EBB671,DE091A,000000,00ffff&chds=a&chdl=%s&chd=t:%s" /><br>\n')
82DRAW_OPTIONS = ('{passThroughBlack:false,useRowLabels:false,cellWidth:30,'
83                'cellHeight:30}')
84TABLE_OPTIONS = '{showRowNumber:true,firstRowNumber:" ",sort:"disable"}'
85
86def GetFiles(rev, bench_dir, platform):
87  """Reads in bench files of interest into a dictionary.
88
89  If bench_dir is not empty, tries to read in local bench files; otherwise check
90  Google Storage. Filters files by revision (rev) and platform, and ignores
91  non-tile, non-viewport bench files.
92  Outputs dictionary [filename] -> [file content].
93  """
94  file_dic = {}
95  if not bench_dir:
96    uri = boto.storage_uri(URI_BUCKET, GOOGLE_STORAGE_URI_SCHEME)
97    # The boto API does not allow prefix/wildcard matching of Google Storage
98    # objects. And Google Storage has a flat structure instead of being
99    # organized in directories. Therefore, we have to scan all objects in the
100    # Google Storage bucket to find the files we need, which is slow.
101    # The option of implementing prefix matching as in gsutil seems to be
102    # overkill, but gsutil does not provide an API ready for use. If speed is a
103    # big concern, we suggest copying bot bench data from Google Storage using
104    # gsutil and use --log_dir for fast local data reading.
105    for obj in uri.get_bucket():
106      # Filters out files of no interest.
107      if (not obj.name.startswith(GOOGLE_STORAGE_OBJECT_NAME_PREFIX) or
108          (obj.name.find(TILING_FILE_NAME_INDICATOR) < 0 and
109           obj.name.find(VIEWPORT_FILE_NAME_INDICATOR) < 0) or
110          obj.name.find(platform) < 0 or
111          obj.name.find(BENCH_FILE_PREFIX_TEMPLATE % rev) < 0):
112        continue
113      file_dic[
114          obj.name[obj.name.rfind('/') + 1 : ]] = obj.get_contents_as_string()
115  else:
116    for f in os.listdir(bench_dir):
117      if (not os.path.isfile(os.path.join(bench_dir, f)) or
118          (f.find(TILING_FILE_NAME_INDICATOR) < 0 and
119           f.find(VIEWPORT_FILE_NAME_INDICATOR) < 0) or
120          not f.startswith(BENCH_FILE_PREFIX_TEMPLATE % rev)):
121        continue
122      file_dic[f] = open(os.path.join(bench_dir, f)).read()
123
124  if not file_dic:
125    raise Exception('No bench file found in "%s" or Google Storage.' %
126                    bench_dir)
127
128  return file_dic
129
130def GetTileMatrix(layout, tile_size, values, viewport):
131  """For the given tile layout and per-tile bench values, returns a matrix of
132  bench values with tiles outside the given viewport set to 0.
133
134  layout, tile_size and viewport are given in string of format <w>x<h>, where
135  <w> is viewport width or number of tile columns, and <h> is viewport height or
136  number of tile rows. We truncate tile rows to MAX_TILE_ROWS to adjust for very
137  long skp's.
138
139  values: per-tile benches ordered row-by-row, starting from the top-left tile.
140
141  Returns [sum, matrix] where sum is the total bench tile time that covers the
142  viewport, and matrix is used for visualizing the tiles.
143  """
144  [tile_cols, tile_rows] = [int(i) for i in layout.split('x')]
145  [tile_x, tile_y] = [int(i) for i in tile_size.split('x')]
146  [viewport_x, viewport_y] = [int(i) for i in viewport.split('x')]
147  viewport_cols = int(math.ceil(viewport_x * 1.0 / tile_x))
148  viewport_rows = int(math.ceil(viewport_y * 1.0 / tile_y))
149  truncated_tile_rows = min(tile_rows, MAX_TILE_ROWS)
150
151  viewport_tile_sum = 0
152  matrix = [[0 for y in range(tile_cols)] for x in range(truncated_tile_rows)]
153  for y in range(min(viewport_cols, tile_cols)):
154    for x in range(min(truncated_tile_rows, viewport_rows)):
155      matrix[x][y] = values[x * tile_cols + y]
156      viewport_tile_sum += values[x * tile_cols + y]
157
158  return [viewport_tile_sum, matrix]
159
160def GetTileVisCodes(suffix, matrix):
161  """Generates and returns strings of [js_codes, row1, row2] which are codes for
162  visualizing the benches from the given tile config and matrix data.
163  row1 is used for the first row of heatmaps; row2 is for corresponding tables.
164  suffix is only used to avoid name conflicts in the whole html output.
165  """
166  this_js = 'var data_%s=new google.visualization.DataTable();' % suffix
167  for i in range(len(matrix[0])):
168    this_js += 'data_%s.addColumn("number","%s");' % (suffix, i)
169  this_js += 'data_%s.addRows(%s);' % (suffix, str(matrix))
170  # Adds heatmap chart.
171  this_js += ('var heat_%s=new org.systemsbiology.visualization' % suffix +
172              '.BioHeatMap(document.getElementById("%s"));' % suffix +
173              'heat_%s.draw(data_%s,%s);' % (suffix, suffix, DRAW_OPTIONS))
174  # Adds data table chart.
175  this_js += ('var table_%s=new google.visualization.Table(document.' % suffix +
176              'getElementById("t%s"));table_%s.draw(data_%s,%s);\n' % (
177                  suffix, suffix, suffix, TABLE_OPTIONS))
178  table_row1 = '<td>%s<div id="%s"></div></td>' % (suffix, suffix)
179  table_row2 = '<td><div id="t%s"></div></td>' % suffix
180
181  return [this_js, table_row1, table_row2]
182
183def OutputTileAnalysis(rev, representation_alg, bench_dir, platform):
184  """Reads skp bench data and outputs tile vs. viewport analysis for the given
185  platform.
186
187  Ignores data with revisions other than rev. If bench_dir is not empty, read
188  from the local directory instead of Google Storage.
189  Uses the provided representation_alg for calculating bench representations.
190
191  Returns (js_codes, body_codes): strings of js/html codes for stats and
192  visualization.
193  """
194  js_codes = ''
195  body_codes = ('}</script></head><body>'
196                '<h3>PLATFORM: %s REVISION: %s</h3><br>' % (platform, rev))
197  bench_dic = {}  # [bench][config] -> [layout, [values]]
198  file_dic = GetFiles(rev, bench_dir, platform)
199  for f in file_dic:
200    for point in bench_util.parse('', file_dic[f].split('\n'),
201                                  representation_alg):
202      if point.time_type:  # Ignores non-walltime time_type.
203        continue
204      bench = point.bench.replace('.skp', '')
205      config = point.config.replace('simple_', '')
206      components = config.split('_')
207      if components[0] == 'viewport':
208        bench_dic.setdefault(bench, {})[config] = [components[1], [point.time]]
209      else:  # Stores per-tile benches.
210        bench_dic.setdefault(bench, {})[config] = [
211          point.tile_layout, point.per_tile_values]
212  benches = bench_dic.keys()
213  benches.sort()
214  for bench in benches:
215    body_codes += '<h4>%s</h4><br><table><tr>' % bench
216    heat_plots = ''  # For table row of heatmap plots.
217    table_plots = ''  # For table row of data table plots.
218    # For bar plot legends and values in URL string.
219    legends = ''
220    values = ''
221    keys = bench_dic[bench].keys()
222    keys.sort()
223    if not keys[-1].startswith('viewport'):  # No viewport to analyze; skip.
224      continue
225    else:
226      # Extracts viewport size, which for all viewport configs is the same.
227      viewport = bench_dic[bench][keys[-1]][0]
228    for config in keys:
229      [layout, value_li] = bench_dic[bench][config]
230      if config.startswith('tile_'):  # For per-tile data, visualize tiles.
231        tile_size = config.split('_')[1]
232        if (not re.search(DIMENSIONS_RE, layout) or
233            not re.search(DIMENSIONS_RE, tile_size) or
234            not re.search(DIMENSIONS_RE, viewport)):
235          continue  # Skip unrecognized formats.
236        [viewport_tile_sum, matrix] = GetTileMatrix(
237            layout, tile_size, value_li, viewport)
238        values += '%s|' % viewport_tile_sum
239        [this_js, row1, row2] = GetTileVisCodes(config + '_' + bench, matrix)
240        heat_plots += row1
241        table_plots += row2
242        js_codes += this_js
243      else:  # For viewport data, there is only one element in value_li.
244        values += '%s|' % sum(value_li)
245      legends += '%s:%s|' % (config, sum(value_li))
246    body_codes += (heat_plots + '</tr><tr>' + table_plots + '</tr></table>' +
247                   '<br>' + BAR_CHART_TEMPLATE % (legends[:-1], values[:-1]))
248
249  return (js_codes, body_codes)
250
251def main():
252  """Parses flags and outputs expected Skia picture bench results."""
253  parser = optparse.OptionParser(USAGE_STRING % '%prog' + HELP_STRING)
254  parser.add_option(OPTION_PLATFORM_SHORT, OPTION_PLATFORM,
255      dest='plat', default=DEFAULT_PLATFORM,
256      help='Platform to analyze. Set to DEFAULT_PLATFORM if not given.')
257  parser.add_option(OPTION_REVISION_SHORT, OPTION_REVISION,
258      dest='rev',
259      help='(Mandatory) revision number to analyze.')
260  parser.add_option(OPTION_DIR_SHORT, OPTION_DIR,
261      dest='log_dir', default='',
262      help=('(Optional) local directory where bench log files reside. If left '
263            'empty (by default), will try to read from Google Storage.'))
264  parser.add_option(OPTION_REPRESENTATION_ALG_SHORT, OPTION_REPRESENTATION_ALG,
265      dest='alg', default=REPRESENTATION_ALG,
266      help=('Bench representation algorithm. '
267            'Default to "%s".' % REPRESENTATION_ALG))
268  (options, args) = parser.parse_args()
269  if not (options.rev and options.rev.isdigit()):
270    parser.error('Please provide correct mandatory flag %s' % OPTION_REVISION)
271    return
272  rev = int(options.rev)
273  (js_codes, body_codes) = OutputTileAnalysis(
274      rev, options.alg, options.log_dir, options.plat)
275  print HTML_PREFIX + js_codes + body_codes + HTML_SUFFIX
276
277
278if '__main__' == __name__:
279  main()
280