Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Defines various log processors used by buildbot steps. | 6 """Defines various log processors used by buildbot steps. |
| 7 | 7 |
| 8 Current approach is to set an instance of log processor in | 8 Current approach is to set an instance of log processor in |
| 9 the ProcessLogTestStep implementation and it will call process() | 9 the ProcessLogTestStep implementation and it will call process() |
| 10 method upon completion with full data from process stdio. | 10 method upon completion with full data from process stdio. |
| 11 """ | 11 """ |
| 12 | 12 |
| 13 import errno | 13 import errno |
| 14 import logging | 14 import logging |
| 15 import os | 15 import os |
| 16 import re | 16 import re |
| 17 import simplejson | 17 import simplejson |
| 18 | 18 |
| 19 import chromium_config as config | 19 import chromium_config as config |
| 20 import chromium_utils | 20 import chromium_utils |
| 21 | 21 |
| 22 from buildbot.status import builder | |
| 23 | |
| 22 READABLE_FILE_PERMISSIONS = int('644', 8) | 24 READABLE_FILE_PERMISSIONS = int('644', 8) |
| 23 EXECUTABLE_FILE_PERMISSIONS = int('755', 8) | 25 EXECUTABLE_FILE_PERMISSIONS = int('755', 8) |
| 24 | 26 |
| 25 # For the GraphingLogProcessor, the file into which it will save a list | 27 # For the GraphingLogProcessor, the file into which it will save a list |
| 26 # of graph names for use by the JS doing the plotting. | 28 # of graph names for use by the JS doing the plotting. |
| 27 GRAPH_LIST = config.Master.perf_graph_list | 29 GRAPH_LIST = config.Master.perf_graph_list |
| 28 | 30 |
| 31 # perf_expectations.json holds performance expectations. It is a | |
| 32 # JSON-formatted file with this format: | |
| 33 # | |
| 34 # {PERFID: { | |
| 35 # RESULTTYPE: {"delta": DELTA, "var": VAR}, | |
| 36 # RESULTTYPE: {"delta": DELTA, "var": VAR}, | |
| 37 # ... | |
| 38 # }, | |
| 39 # ..., | |
| 40 # "loaded": true | |
| 41 # } | |
| 42 # | |
| 43 # PERFID (string): | |
| 44 # Perf mapping identifier of the form "build-perf-name/test-name", | |
| 45 # (see factory/chromium_commands.py). | |
| 46 # | |
| 47 # RESULTTYPE (string): | |
| 48 # A particular trace within a test (ie. t, vm_rss_f_r). | |
| 49 # | |
| 50 # DELTA (integer): | |
| 51 # Delta tolerance (test - ref). | |
| 52 # | |
| 53 # VAR (integer): | |
| 54 # Variance tolerance. | |
| 55 # | |
| 56 # Notes: | |
| 57 # - Strings are quoted with " (not '). | |
| 58 # - Comments are not allowed in JSON. | |
| 59 # | |
| 60 PERF_EXPECTATIONS = ('../scripts/master/log_parser/perf_expectations/' + | |
| 61 'perf_expectations.json') | |
| 62 | |
| 29 def FormatFloat(number): | 63 def FormatFloat(number): |
| 30 """Formats float with two decimal points.""" | 64 """Formats float with two decimal points.""" |
| 31 if number: | 65 if number: |
| 32 return '%.2f' % number | 66 return '%.2f' % number |
| 33 else: | 67 else: |
| 34 return '0.00' | 68 return '0.00' |
| 35 | 69 |
| 36 | 70 |
| 37 def Prepend(filename, data): | 71 def Prepend(filename, data): |
| 38 chromium_utils.Prepend(filename, data) | 72 chromium_utils.Prepend(filename, data) |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 66 if digits >= 100: | 100 if digits >= 100: |
| 67 # Don't append a meaningless '.0' to an integer number. | 101 # Don't append a meaningless '.0' to an integer number. |
| 68 digits = int(digits) | 102 digits = int(digits) |
| 69 # Exponent is now divisible by 3, between -3 and 6 inclusive: (-3, 0, 3, 6). | 103 # Exponent is now divisible by 3, between -3 and 6 inclusive: (-3, 0, 3, 6). |
| 70 return '%s%s' % (digits, METRIC_SUFFIX[exponent]) | 104 return '%s%s' % (digits, METRIC_SUFFIX[exponent]) |
| 71 | 105 |
| 72 | 106 |
| 73 class PerformanceLogProcessor(object): | 107 class PerformanceLogProcessor(object): |
| 74 """ Parent class for performance log parsers. """ | 108 """ Parent class for performance log parsers. """ |
| 75 | 109 |
| 76 def __init__(self, report_link=None, output_dir=None): | 110 def __init__(self, report_link=None, output_dir=None, perf_name=None): |
| 77 self._report_link = report_link | 111 self._report_link = report_link |
| 78 if output_dir is None: | 112 if output_dir is None: |
| 79 output_dir = os.getcwd() | 113 output_dir = os.getcwd() |
| 80 elif output_dir.startswith('~'): | 114 elif output_dir.startswith('~'): |
| 81 output_dir = os.path.expanduser(output_dir) | 115 output_dir = os.path.expanduser(output_dir) |
| 82 self._output_dir = output_dir | 116 self._output_dir = output_dir |
| 83 self._matches = {} | 117 self._matches = {} |
| 84 | 118 |
| 119 # Performance regression/speedup alerts. | |
| 120 self._perf_name = perf_name | |
| 121 self._actual_performance = None | |
| 122 self._expected_performance = None | |
| 123 self._result_types = [] | |
| 124 self._perf_regress = [] | |
| 125 self._var_regress = [] | |
| 126 self._perf_improve = [] | |
| 127 self._var_improve = [] | |
| 128 | |
| 85 # The revision isn't known until the Process() call. | 129 # The revision isn't known until the Process() call. |
| 86 self._revision = -1 | 130 self._revision = -1 |
| 87 | 131 |
| 132 def LoadPerformanceExpectations(self): | |
| 133 self._expected = {} | |
| 134 try: | |
| 135 perf_file = open(PERF_EXPECTATIONS, 'r') | |
| 136 except IOError, e: | |
| 137 raise | |
|
M-A Ruel
2009/08/31 20:31:12
That seems quite useless
| |
| 138 | |
| 139 perf_list = [] | |
| 140 if perf_file: | |
| 141 try: | |
| 142 perf_list = simplejson.load(perf_file) | |
| 143 except ValueError: | |
| 144 perf_file.seek(0) | |
| 145 logging.error("Error parsing %s: '%s'" % (PERF_EXPECTATIONS, | |
| 146 perf_file.read().strip())) | |
|
M-A Ruel
2009/08/31 20:31:12
I'm not sure it's a good idea to read the file aft
| |
| 147 perf_file.close() | |
| 148 | |
| 149 # Find this perf/test entry | |
| 150 if perf_list and perf_list.has_key(self._perf_name): | |
| 151 self._expected_performance = perf_list[self._perf_name] | |
| 152 | |
| 153 def PerformanceChangesAsText(self): | |
| 154 text = [] | |
| 155 | |
| 156 if len(self._perf_regress) > 0: | |
| 157 text.append("PERF_REGRESS: " + ', '.join(self._perf_regress)) | |
| 158 | |
| 159 if len(self._var_regress) > 0: | |
| 160 text.append("VAR_REGRESS: " + ', '.join(self._var_regress)) | |
| 161 | |
| 162 if len(self._perf_improve) > 0: | |
| 163 text.append("PERF_IMPROVE: " + ', '.join(self._perf_improve)) | |
| 164 | |
| 165 if len(self._var_improve) > 0: | |
| 166 text.append("VAR_IMPROVE: " + ', '.join(self._var_improve)) | |
| 167 | |
| 168 return text | |
| 169 | |
| 170 def PerformanceChanges(self): | |
| 171 # Load performance expectations for this test. | |
| 172 self.LoadPerformanceExpectations() | |
| 173 | |
| 174 # Return if no performance expectations or results were found. | |
| 175 if not self._expected_performance or not self._actual_performance: | |
| 176 return [] | |
| 177 | |
| 178 # Compare actual and expected results. | |
| 179 for type in self._result_types: | |
| 180 if not (self._expected_performance.has_key(type) and | |
| 181 self._actual_performance.has_key(type)): | |
| 182 # Skip result types we don't know about. | |
| 183 continue | |
| 184 | |
| 185 expect = self._expected_performance[type] | |
| 186 actual = self._actual_performance[type] | |
| 187 | |
| 188 # Set the high and low performance and variance tolerances. The actual | |
| 189 # delta and variance needs to be within 50% above and below this range to | |
| 190 # keep the performance test green. If the results fall above or below | |
| 191 # this range, the test will go red (signaling a regression) or orange | |
| 192 # (signaling a speedup). | |
| 193 high_perf = (expect['delta'] + 1.5*expect['var']) | |
| 194 low_perf = (expect['delta'] - 1.5*expect['var']) | |
| 195 high_var = 1.5*expect['var'] | |
| 196 low_var = 0.5*expect['var'] | |
| 197 | |
| 198 if actual['delta'] > high_perf: | |
| 199 self._perf_regress.append(type) | |
| 200 elif actual['delta'] < low_perf: | |
| 201 self._perf_improve.append(type) | |
| 202 | |
| 203 if actual['var'] > high_var: | |
| 204 self._var_regress.append(type) | |
| 205 elif actual['var'] < low_var: | |
| 206 self._var_improve.append(type) | |
| 207 | |
| 208 return self.PerformanceChangesAsText() | |
| 209 | |
| 210 def evaluateCommand(self, cmd): | |
| 211 if len(self._perf_regress) > 0 or len(self._var_regress) > 0: | |
| 212 return builder.FAILURE | |
| 213 | |
| 214 if len(self._perf_improve) > 0 or len(self._var_improve) > 0: | |
| 215 return builder.WARNINGS | |
| 216 | |
| 217 # There was no change in performance, report success. | |
| 218 return builder.SUCCESS | |
| 219 | |
| 88 def Process(self, revision, data): | 220 def Process(self, revision, data): |
| 89 """Invoked by the step with data from log file once it completes. | 221 """Invoked by the step with data from log file once it completes. |
| 90 | 222 |
| 91 Each subclass needs to override this method to provide custom logic, | 223 Each subclass needs to override this method to provide custom logic, |
| 92 which should include setting self._revision. | 224 which should include setting self._revision. |
| 93 Args: | 225 Args: |
| 94 revision: changeset revision number that triggered the build. | 226 revision: changeset revision number that triggered the build. |
| 95 data: content of the log file that needs to be processed. | 227 data: content of the log file that needs to be processed. |
| 96 | 228 |
| 97 Returns: | 229 Returns: |
| (...skipping 238 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 336 | 468 |
| 337 self._MakeOutputDirectory() | 469 self._MakeOutputDirectory() |
| 338 | 470 |
| 339 # Parse the log and fill _graphs. | 471 # Parse the log and fill _graphs. |
| 340 for log_line in data.splitlines(): | 472 for log_line in data.splitlines(): |
| 341 self._ProcessLine(log_line) | 473 self._ProcessLine(log_line) |
| 342 | 474 |
| 343 self.__CreateSummaryOutput() | 475 self.__CreateSummaryOutput() |
| 344 if self._ShouldWriteResults(): | 476 if self._ShouldWriteResults(): |
| 345 self.__SaveGraphInfo() | 477 self.__SaveGraphInfo() |
| 346 return self._text_summary | 478 return self.PerformanceChanges() + self._text_summary |
| 347 | 479 |
| 348 def _ProcessLine(self, line): | 480 def _ProcessLine(self, line): |
| 349 line_match = self.RESULTS_REGEX.match(line) | 481 line_match = self.RESULTS_REGEX.match(line) |
| 350 if line_match: | 482 if line_match: |
| 351 match_dict = line_match.groupdict() | 483 match_dict = line_match.groupdict() |
| 352 graph_name = match_dict['GRAPH'].strip() | 484 graph_name = match_dict['GRAPH'].strip() |
| 353 trace_name = match_dict['TRACE'].strip() | 485 trace_name = match_dict['TRACE'].strip() |
| 354 | 486 |
| 355 graph = self._graphs.get(graph_name, Graph()) | 487 graph = self._graphs.get(graph_name, Graph()) |
| 356 graph.units = match_dict['UNITS'] or '' | 488 graph.units = match_dict['UNITS'] or '' |
| (...skipping 24 matching lines...) Expand all Loading... | |
| 381 else: | 513 else: |
| 382 try: | 514 try: |
| 383 trace.value = float(trace.value) | 515 trace.value = float(trace.value) |
| 384 except ValueError: | 516 except ValueError: |
| 385 logging.warning("Bad test output: '%s'" % trace.value.strip()) | 517 logging.warning("Bad test output: '%s'" % trace.value.strip()) |
| 386 return | 518 return |
| 387 | 519 |
| 388 graph.traces[trace_name] = trace | 520 graph.traces[trace_name] = trace |
| 389 self._graphs[graph_name] = graph | 521 self._graphs[graph_name] = graph |
| 390 | 522 |
| 523 # Set actual performance data when we come across useful values. | |
| 524 # | |
| 525 # trace_name will be of the form "RESULTTYPE" or "RESULTTYPE_ref". | |
| 526 # A trace with _ref in its name refers to a reference build. | |
| 527 # | |
| 528 # Common result types for page cyclers: t, vm_rss_f_r, IO_b_b, etc. | |
| 529 # A test's result types vary between test types. Currently, a test | |
| 530 # only needs to output the appropriate text format to embed a new | |
| 531 # result type. | |
| 532 | |
| 533 m = re.match(r"^(\w+)_ref$", trace_name) | |
| 534 if m: | |
| 535 is_ref_build = True | |
| 536 result_type = m.group(1) | |
| 537 else: | |
| 538 is_ref_build = False | |
| 539 result_type = trace_name | |
| 540 | |
| 541 if not self._actual_performance: | |
| 542 self._actual_performance = {} | |
| 543 | |
| 544 if not self._actual_performance.has_key(result_type): | |
| 545 self._actual_performance[result_type] = {} | |
| 546 | |
| 547 actual = self._actual_performance[result_type] | |
| 548 if is_ref_build: | |
| 549 actual['ref'] = trace.value | |
| 550 else: | |
| 551 actual['test'] = trace.value | |
| 552 actual['var'] = trace.stddev | |
| 553 | |
| 554 # If we have both the test and ref results, compute the delta for this | |
| 555 # result type. | |
| 556 if actual.has_key('test') and actual.has_key('ref'): | |
| 557 self._result_types.append(result_type) | |
| 558 actual['delta'] = actual['test'] - actual['ref'] | |
| 559 | |
| 391 def _CalculateStatistics(self, value_list, trace_name): | 560 def _CalculateStatistics(self, value_list, trace_name): |
| 392 """Returns a tuple (mean, standard deviation) from a list of values. | 561 """Returns a tuple (mean, standard deviation) from a list of values. |
| 393 | 562 |
| 394 This method may be overridden by subclasses wanting a different standard | 563 This method may be overridden by subclasses wanting a different standard |
| 395 deviation calcuation (or some other sort of error value entirely). | 564 deviation calcuation (or some other sort of error value entirely). |
| 396 | 565 |
| 397 Args: | 566 Args: |
| 398 value_list: the list of values to use in the calculation | 567 value_list: the list of values to use in the calculation |
| 399 trace_name: the trace that produced the data (not used in the base | 568 trace_name: the trace that produced the data (not used in the base |
| 400 implementation, but subclasses may use it) | 569 implementation, but subclasses may use it) |
| (...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 582 FormatFloat(mean), | 751 FormatFloat(mean), |
| 583 FormatFloat(stddev), | 752 FormatFloat(stddev), |
| 584 self._JoinWithSpacesAndNewLine(times))) | 753 self._JoinWithSpacesAndNewLine(times))) |
| 585 | 754 |
| 586 filename = os.path.join(self._output_dir, | 755 filename = os.path.join(self._output_dir, |
| 587 '%s_%s.dat' % (self._revision, trace_name)) | 756 '%s_%s.dat' % (self._revision, trace_name)) |
| 588 file = open(filename, 'w') | 757 file = open(filename, 'w') |
| 589 file.write(''.join(file_data)) | 758 file.write(''.join(file_data)) |
| 590 file.close() | 759 file.close() |
| 591 os.chmod(filename, READABLE_FILE_PERMISSIONS) | 760 os.chmod(filename, READABLE_FILE_PERMISSIONS) |
| OLD | NEW |