util.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. """util.py - General utilities for running, loading, and processing benchmarks"""
  2. import json
  3. import os
  4. import re
  5. import subprocess
  6. import sys
  7. import tempfile
  8. # Input file type enumeration
  9. IT_Invalid = 0
  10. IT_JSON = 1
  11. IT_Executable = 2
  12. _num_magic_bytes = 2 if sys.platform.startswith("win") else 4
  13. def is_executable_file(filename):
  14. """
  15. Return 'True' if 'filename' names a valid file which is likely
  16. an executable. A file is considered an executable if it starts with the
  17. magic bytes for a EXE, Mach O, or ELF file.
  18. """
  19. if not os.path.isfile(filename):
  20. return False
  21. with open(filename, mode="rb") as f:
  22. magic_bytes = f.read(_num_magic_bytes)
  23. if sys.platform == "darwin":
  24. return magic_bytes in [
  25. b"\xfe\xed\xfa\xce", # MH_MAGIC
  26. b"\xce\xfa\xed\xfe", # MH_CIGAM
  27. b"\xfe\xed\xfa\xcf", # MH_MAGIC_64
  28. b"\xcf\xfa\xed\xfe", # MH_CIGAM_64
  29. b"\xca\xfe\xba\xbe", # FAT_MAGIC
  30. b"\xbe\xba\xfe\xca", # FAT_CIGAM
  31. ]
  32. elif sys.platform.startswith("win"):
  33. return magic_bytes == b"MZ"
  34. else:
  35. return magic_bytes == b"\x7fELF"
  36. def is_json_file(filename):
  37. """
  38. Returns 'True' if 'filename' names a valid JSON output file.
  39. 'False' otherwise.
  40. """
  41. try:
  42. with open(filename, "r") as f:
  43. json.load(f)
  44. return True
  45. except BaseException:
  46. pass
  47. return False
  48. def classify_input_file(filename):
  49. """
  50. Return a tuple (type, msg) where 'type' specifies the classified type
  51. of 'filename'. If 'type' is 'IT_Invalid' then 'msg' is a human readable
  52. string representing the error.
  53. """
  54. ftype = IT_Invalid
  55. err_msg = None
  56. if not os.path.exists(filename):
  57. err_msg = "'%s' does not exist" % filename
  58. elif not os.path.isfile(filename):
  59. err_msg = "'%s' does not name a file" % filename
  60. elif is_executable_file(filename):
  61. ftype = IT_Executable
  62. elif is_json_file(filename):
  63. ftype = IT_JSON
  64. else:
  65. err_msg = (
  66. "'%s' does not name a valid benchmark executable or JSON file"
  67. % filename
  68. )
  69. return ftype, err_msg
  70. def check_input_file(filename):
  71. """
  72. Classify the file named by 'filename' and return the classification.
  73. If the file is classified as 'IT_Invalid' print an error message and exit
  74. the program.
  75. """
  76. ftype, msg = classify_input_file(filename)
  77. if ftype == IT_Invalid:
  78. print("Invalid input file: %s" % msg)
  79. sys.exit(1)
  80. return ftype
  81. def find_benchmark_flag(prefix, benchmark_flags):
  82. """
  83. Search the specified list of flags for a flag matching `<prefix><arg>` and
  84. if it is found return the arg it specifies. If specified more than once the
  85. last value is returned. If the flag is not found None is returned.
  86. """
  87. assert prefix.startswith("--") and prefix.endswith("=")
  88. result = None
  89. for f in benchmark_flags:
  90. if f.startswith(prefix):
  91. result = f[len(prefix) :]
  92. return result
  93. def remove_benchmark_flags(prefix, benchmark_flags):
  94. """
  95. Return a new list containing the specified benchmark_flags except those
  96. with the specified prefix.
  97. """
  98. assert prefix.startswith("--") and prefix.endswith("=")
  99. return [f for f in benchmark_flags if not f.startswith(prefix)]
  100. def load_benchmark_results(fname, benchmark_filter):
  101. """
  102. Read benchmark output from a file and return the JSON object.
  103. Apply benchmark_filter, a regular expression, with nearly the same
  104. semantics of the --benchmark_filter argument. May be None.
  105. Note: the Python regular expression engine is used instead of the
  106. one used by the C++ code, which may produce different results
  107. in complex cases.
  108. REQUIRES: 'fname' names a file containing JSON benchmark output.
  109. """
  110. def benchmark_wanted(benchmark):
  111. if benchmark_filter is None:
  112. return True
  113. name = benchmark.get("run_name", None) or benchmark["name"]
  114. return re.search(benchmark_filter, name) is not None
  115. with open(fname, "r") as f:
  116. results = json.load(f)
  117. if "context" in results:
  118. if "json_schema_version" in results["context"]:
  119. json_schema_version = results["context"]["json_schema_version"]
  120. if json_schema_version != 1:
  121. print(
  122. "In %s, got unnsupported JSON schema version: %i, expected 1"
  123. % (fname, json_schema_version)
  124. )
  125. sys.exit(1)
  126. if "benchmarks" in results:
  127. results["benchmarks"] = list(
  128. filter(benchmark_wanted, results["benchmarks"])
  129. )
  130. return results
  131. def sort_benchmark_results(result):
  132. benchmarks = result["benchmarks"]
  133. # From inner key to the outer key!
  134. benchmarks = sorted(
  135. benchmarks,
  136. key=lambda benchmark: benchmark["repetition_index"]
  137. if "repetition_index" in benchmark
  138. else -1,
  139. )
  140. benchmarks = sorted(
  141. benchmarks,
  142. key=lambda benchmark: 1
  143. if "run_type" in benchmark and benchmark["run_type"] == "aggregate"
  144. else 0,
  145. )
  146. benchmarks = sorted(
  147. benchmarks,
  148. key=lambda benchmark: benchmark["per_family_instance_index"]
  149. if "per_family_instance_index" in benchmark
  150. else -1,
  151. )
  152. benchmarks = sorted(
  153. benchmarks,
  154. key=lambda benchmark: benchmark["family_index"]
  155. if "family_index" in benchmark
  156. else -1,
  157. )
  158. result["benchmarks"] = benchmarks
  159. return result
  160. def run_benchmark(exe_name, benchmark_flags):
  161. """
  162. Run a benchmark specified by 'exe_name' with the specified
  163. 'benchmark_flags'. The benchmark is run directly as a subprocess to preserve
  164. real time console output.
  165. RETURNS: A JSON object representing the benchmark output
  166. """
  167. output_name = find_benchmark_flag("--benchmark_out=", benchmark_flags)
  168. is_temp_output = False
  169. if output_name is None:
  170. is_temp_output = True
  171. thandle, output_name = tempfile.mkstemp()
  172. os.close(thandle)
  173. benchmark_flags = list(benchmark_flags) + [
  174. "--benchmark_out=%s" % output_name
  175. ]
  176. cmd = [exe_name] + benchmark_flags
  177. print("RUNNING: %s" % " ".join(cmd))
  178. exitCode = subprocess.call(cmd)
  179. if exitCode != 0:
  180. print("TEST FAILED...")
  181. sys.exit(exitCode)
  182. json_res = load_benchmark_results(output_name, None)
  183. if is_temp_output:
  184. os.unlink(output_name)
  185. return json_res
  186. def run_or_load_benchmark(filename, benchmark_flags):
  187. """
  188. Get the results for a specified benchmark. If 'filename' specifies
  189. an executable benchmark then the results are generated by running the
  190. benchmark. Otherwise 'filename' must name a valid JSON output file,
  191. which is loaded and the result returned.
  192. """
  193. ftype = check_input_file(filename)
  194. if ftype == IT_JSON:
  195. benchmark_filter = find_benchmark_flag(
  196. "--benchmark_filter=", benchmark_flags
  197. )
  198. return load_benchmark_results(filename, benchmark_filter)
  199. if ftype == IT_Executable:
  200. return run_benchmark(filename, benchmark_flags)
  201. raise ValueError("Unknown file type %s" % ftype)