Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(124)

Side by Side Diff: log_parser/process_log.py

Issue 177028: Detect perf regressions and speedups automatically.... (Closed) Base URL: svn://chrome-svn.corp.google.com/chrome/trunk/tools/buildbot/scripts/master/
Patch Set: '' Created 11 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « factory/commands.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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)
OLDNEW
« no previous file with comments | « factory/commands.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698