check_bench_regressions.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. '''
  2. Created on May 16, 2011
  3. @author: bungeman
  4. '''
  5. import bench_util
  6. import getopt
  7. import httplib
  8. import itertools
  9. import json
  10. import os
  11. import re
  12. import sys
  13. import urllib
  14. import urllib2
  15. import xml.sax.saxutils
  16. # Maximum expected number of characters we expect in an svn revision.
  17. MAX_SVN_REV_LENGTH = 5
  18. # Indices for getting elements from bench expectation files.
  19. # See bench_expectations_<builder>.txt for details.
  20. EXPECTED_IDX = -3
  21. LB_IDX = -2
  22. UB_IDX = -1
  23. # Indices of the tuple of dictionaries containing slower and faster alerts.
  24. SLOWER = 0
  25. FASTER = 1
  26. # URL prefix for the bench dashboard page. Showing recent 15 days of data.
  27. DASHBOARD_URL_PREFIX = 'http://go/skpdash/#15'
  28. def usage():
  29. """Prints simple usage information."""
  30. print '-a <representation_alg> bench representation algorithm to use. '
  31. print ' Defaults to "25th". See bench_util.py for details.'
  32. print '-b <builder> name of the builder whose bench data we are checking.'
  33. print '-d <dir> a directory containing bench_<revision>_<scalar> files.'
  34. print '-e <file> file containing expected bench builder values/ranges.'
  35. print ' Will raise exception if actual bench values are out of range.'
  36. print ' See bench_expectations_<builder>.txt for data format / examples.'
  37. print '-r <revision> the git commit hash or svn revision for checking '
  38. print ' bench values.'
  39. class Label:
  40. """The information in a label.
  41. (str, str, str, str, {str:str})"""
  42. def __init__(self, bench, config, time_type, settings):
  43. self.bench = bench
  44. self.config = config
  45. self.time_type = time_type
  46. self.settings = settings
  47. def __repr__(self):
  48. return "Label(%s, %s, %s, %s)" % (
  49. str(self.bench),
  50. str(self.config),
  51. str(self.time_type),
  52. str(self.settings),
  53. )
  54. def __str__(self):
  55. return "%s_%s_%s_%s" % (
  56. str(self.bench),
  57. str(self.config),
  58. str(self.time_type),
  59. str(self.settings),
  60. )
  61. def __eq__(self, other):
  62. return (self.bench == other.bench and
  63. self.config == other.config and
  64. self.time_type == other.time_type and
  65. self.settings == other.settings)
  66. def __hash__(self):
  67. return (hash(self.bench) ^
  68. hash(self.config) ^
  69. hash(self.time_type) ^
  70. hash(frozenset(self.settings.iteritems())))
  71. def create_bench_dict(revision_data_points):
  72. """Convert current revision data into a dictionary of line data.
  73. Args:
  74. revision_data_points: a list of bench data points
  75. Returns:
  76. a dictionary of this form:
  77. keys = Label objects
  78. values = the corresponding bench value
  79. """
  80. bench_dict = {}
  81. for point in revision_data_points:
  82. point_name = Label(point.bench,point.config,point.time_type,
  83. point.settings)
  84. if point_name not in bench_dict:
  85. bench_dict[point_name] = point.time
  86. else:
  87. raise Exception('Duplicate expectation entry: ' + str(point_name))
  88. return bench_dict
  89. def read_expectations(expectations, filename):
  90. """Reads expectations data from file and put in expectations dict."""
  91. for expectation in open(filename).readlines():
  92. elements = expectation.strip().split(',')
  93. if not elements[0] or elements[0].startswith('#'):
  94. continue
  95. if len(elements) != 5:
  96. raise Exception("Invalid expectation line format: %s" %
  97. expectation)
  98. bench_entry = elements[0] + ',' + elements[1]
  99. if bench_entry in expectations:
  100. raise Exception("Dup entries for bench expectation %s" %
  101. bench_entry)
  102. # [<Bench_BmpConfig_TimeType>,<Platform-Alg>] -> (LB, UB, EXPECTED)
  103. expectations[bench_entry] = (float(elements[LB_IDX]),
  104. float(elements[UB_IDX]),
  105. float(elements[EXPECTED_IDX]))
  106. def check_expectations(lines, expectations, key_suffix):
  107. """Check if any bench results are outside of expected range.
  108. For each input line in lines, checks the expectations dictionary to see if
  109. the bench is out of the given range.
  110. Args:
  111. lines: dictionary mapping Label objects to the bench values.
  112. expectations: dictionary returned by read_expectations().
  113. key_suffix: string of <Platform>-<Alg> containing the bot platform and the
  114. bench representation algorithm.
  115. Returns:
  116. No return value.
  117. Raises:
  118. Exception containing bench data that are out of range, if any.
  119. """
  120. # The platform for this bot, to pass to the dashboard plot.
  121. platform = key_suffix[ : key_suffix.rfind('-')]
  122. # Tuple of dictionaries recording exceptions that are slower and faster,
  123. # respectively. Each dictionary maps off_ratio (ratio of actual to expected)
  124. # to a list of corresponding exception messages.
  125. exceptions = ({}, {})
  126. for line in lines:
  127. line_str = str(line)
  128. line_str = line_str[ : line_str.find('_{')]
  129. # Extracts bench and config from line_str, which is in the format
  130. # <bench-picture-name>.skp_<config>_
  131. bench, config = line_str.strip('_').split('.skp_')
  132. bench_platform_key = line_str + ',' + key_suffix
  133. if bench_platform_key not in expectations:
  134. continue
  135. this_bench_value = lines[line]
  136. this_min, this_max, this_expected = expectations[bench_platform_key]
  137. if this_bench_value < this_min or this_bench_value > this_max:
  138. off_ratio = this_bench_value / this_expected
  139. exception = 'Bench %s out of range [%s, %s] (%s vs %s, %s%%).' % (
  140. bench_platform_key, this_min, this_max, this_bench_value,
  141. this_expected, (off_ratio - 1) * 100)
  142. exception += '\n' + '~'.join([
  143. DASHBOARD_URL_PREFIX, bench, platform, config])
  144. if off_ratio > 1: # Bench is slower.
  145. exceptions[SLOWER].setdefault(off_ratio, []).append(exception)
  146. else:
  147. exceptions[FASTER].setdefault(off_ratio, []).append(exception)
  148. outputs = []
  149. for i in [SLOWER, FASTER]:
  150. if exceptions[i]:
  151. ratios = exceptions[i].keys()
  152. ratios.sort(reverse=True)
  153. li = []
  154. for ratio in ratios:
  155. li.extend(exceptions[i][ratio])
  156. header = '%s benches got slower (sorted by %% difference):' % len(li)
  157. if i == FASTER:
  158. header = header.replace('slower', 'faster')
  159. outputs.extend(['', header] + li)
  160. if outputs:
  161. # Directly raising Exception will have stderr outputs tied to the line
  162. # number of the script, so use sys.stderr.write() instead.
  163. # Add a trailing newline to supress new line checking errors.
  164. sys.stderr.write('\n'.join(['Exception:'] + outputs + ['\n']))
  165. exit(1)
  166. def main():
  167. """Parses command line and checks bench expectations."""
  168. try:
  169. opts, _ = getopt.getopt(sys.argv[1:],
  170. "a:b:d:e:r:",
  171. "default-setting=")
  172. except getopt.GetoptError, err:
  173. print str(err)
  174. usage()
  175. sys.exit(2)
  176. directory = None
  177. bench_expectations = {}
  178. rep = '25th' # bench representation algorithm, default to 25th
  179. rev = None # git commit hash or svn revision number
  180. bot = None
  181. try:
  182. for option, value in opts:
  183. if option == "-a":
  184. rep = value
  185. elif option == "-b":
  186. bot = value
  187. elif option == "-d":
  188. directory = value
  189. elif option == "-e":
  190. read_expectations(bench_expectations, value)
  191. elif option == "-r":
  192. rev = value
  193. else:
  194. usage()
  195. assert False, "unhandled option"
  196. except ValueError:
  197. usage()
  198. sys.exit(2)
  199. if directory is None or bot is None or rev is None:
  200. usage()
  201. sys.exit(2)
  202. platform_and_alg = bot + '-' + rep
  203. data_points = bench_util.parse_skp_bench_data(directory, rev, rep)
  204. bench_dict = create_bench_dict(data_points)
  205. if bench_expectations:
  206. check_expectations(bench_dict, bench_expectations, platform_and_alg)
  207. if __name__ == "__main__":
  208. main()