bensong@google.com | ba98f95 | 2013-02-13 23:22:29 +0000 | [diff] [blame] | 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 | |
| 11 | import bench_util |
| 12 | import boto |
| 13 | import math |
| 14 | import optparse |
| 15 | import os |
| 16 | import re |
| 17 | import shutil |
| 18 | |
| 19 | from oauth2_plugin import oauth2_plugin |
| 20 | |
| 21 | # The default platform to analyze. Used when OPTION_PLATFORM flag is not set. |
| 22 | DEFAULT_PLATFORM = 'Nexus10_4-1_Float_Bench_32' |
| 23 | |
| 24 | # Template for gsutil uri. |
| 25 | GOOGLE_STORAGE_URI_SCHEME = 'gs' |
| 26 | URI_BUCKET = 'chromium-skia-gm' |
| 27 | |
| 28 | # Maximum number of rows of tiles to track for viewport covering. |
| 29 | MAX_TILE_ROWS = 8 |
| 30 | |
| 31 | # Constants for optparse. |
| 32 | USAGE_STRING = 'USAGE: %s [options]' |
| 33 | HOWTO_STRING = """ |
| 34 | Note: to read bench data stored in Google Storage, you will need to set up the |
| 35 | corresponding Python library. |
| 36 | See http://developers.google.com/storage/docs/gspythonlibrary for details. |
| 37 | """ |
| 38 | HELP_STRING = """ |
| 39 | For the given platform and revision number, find corresponding viewport and |
| 40 | tile benchmarks for each available picture bench, and output visualization and |
| 41 | analysis in HTML. By default it reads from Skia's Google Storage location where |
| 42 | bot data are stored, but if --dir is given, will read from local directory |
| 43 | instead. |
| 44 | """ + HOWTO_STRING |
| 45 | |
| 46 | OPTION_DIR = '--dir' |
| 47 | OPTION_DIR_SHORT = '-d' |
| 48 | OPTION_REVISION = '--rev' |
| 49 | OPTION_REVISION_SHORT = '-r' |
| 50 | OPTION_PLATFORM = '--platform' |
| 51 | OPTION_PLATFORM_SHORT = '-p' |
| 52 | # Bench representation algorithm flag. |
| 53 | OPTION_REPRESENTATION_ALG = '--algorithm' |
| 54 | OPTION_REPRESENTATION_ALG_SHORT = '-a' |
| 55 | |
| 56 | # Bench representation algorithm. See trunk/bench/bench_util.py. |
| 57 | REPRESENTATION_ALG = bench_util.ALGORITHM_25TH_PERCENTILE |
| 58 | |
| 59 | # Constants for bench file matching. |
| 60 | GOOGLE_STORAGE_OBJECT_NAME_PREFIX = 'perfdata/Skia_' |
| 61 | BENCH_FILE_PREFIX_TEMPLATE = 'bench_r%s_' |
| 62 | TILING_FILE_NAME_INDICATOR = '_tile_' |
| 63 | VIEWPORT_FILE_NAME_INDICATOR = '_viewport_' |
| 64 | |
| 65 | # Regular expression for matching format '<integer>x<integer>'. |
| 66 | DIMENSIONS_RE = '(\d+)x(\d+)' |
| 67 | |
| 68 | # HTML and JS output templates. |
| 69 | HTML_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 |
| 74 | type="text/javascript"> systemsbiology.load("visualization", "1.0", |
| 75 | {packages:["bioheatmap"]});</script><script type="text/javascript"> |
| 76 | google.setOnLoadCallback(drawVisualization); function drawVisualization() { |
| 77 | """ |
| 78 | HTML_SUFFIX = '</body></html>' |
| 79 | BAR_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') |
| 82 | DRAW_OPTIONS = ('{passThroughBlack:false,useRowLabels:false,cellWidth:30,' |
| 83 | 'cellHeight:30}') |
| 84 | TABLE_OPTIONS = '{showRowNumber:true,firstRowNumber:" ",sort:"disable"}' |
| 85 | |
| 86 | def 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 | |
| 130 | def 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 | |
| 160 | def 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 | |
| 183 | def 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 | |
| 251 | def 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 | |
| 278 | if '__main__' == __name__: |
| 279 | main() |