run_microbenchmark.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. #!/usr/bin/env python
  2. # Copyright 2017 gRPC authors.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import cgi
  16. import multiprocessing
  17. import os
  18. import subprocess
  19. import sys
  20. import argparse
  21. import python_utils.jobset as jobset
  22. import python_utils.start_port_server as start_port_server
  23. _AVAILABLE_BENCHMARK_TESTS = ['bm_fullstack_unary_ping_pong',
  24. 'bm_fullstack_streaming_ping_pong',
  25. 'bm_fullstack_streaming_pump',
  26. 'bm_closure',
  27. 'bm_cq',
  28. 'bm_call_create',
  29. 'bm_error',
  30. 'bm_chttp2_hpack',
  31. 'bm_chttp2_transport',
  32. 'bm_pollset',
  33. 'bm_metadata',
  34. 'bm_fullstack_trickle']
  35. flamegraph_dir = os.path.join(os.path.expanduser('~'), 'FlameGraph')
  36. os.chdir(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
  37. if not os.path.exists('reports'):
  38. os.makedirs('reports')
  39. start_port_server.start_port_server()
  40. def fnize(s):
  41. out = ''
  42. for c in s:
  43. if c in '<>, /':
  44. if len(out) and out[-1] == '_': continue
  45. out += '_'
  46. else:
  47. out += c
  48. return out
  49. # index html
  50. index_html = """
  51. <html>
  52. <head>
  53. <title>Microbenchmark Results</title>
  54. </head>
  55. <body>
  56. """
  57. def heading(name):
  58. global index_html
  59. index_html += "<h1>%s</h1>\n" % name
  60. def link(txt, tgt):
  61. global index_html
  62. index_html += "<p><a href=\"%s\">%s</a></p>\n" % (
  63. cgi.escape(tgt, quote=True), cgi.escape(txt))
  64. def text(txt):
  65. global index_html
  66. index_html += "<p><pre>%s</pre></p>\n" % cgi.escape(txt)
  67. def collect_latency(bm_name, args):
  68. """generate latency profiles"""
  69. benchmarks = []
  70. profile_analysis = []
  71. cleanup = []
  72. heading('Latency Profiles: %s' % bm_name)
  73. subprocess.check_call(
  74. ['make', bm_name,
  75. 'CONFIG=basicprof', '-j', '%d' % multiprocessing.cpu_count()])
  76. for line in subprocess.check_output(['bins/basicprof/%s' % bm_name,
  77. '--benchmark_list_tests']).splitlines():
  78. link(line, '%s.txt' % fnize(line))
  79. benchmarks.append(
  80. jobset.JobSpec(['bins/basicprof/%s' % bm_name,
  81. '--benchmark_filter=^%s$' % line,
  82. '--benchmark_min_time=0.05'],
  83. environ={'LATENCY_TRACE': '%s.trace' % fnize(line)}))
  84. profile_analysis.append(
  85. jobset.JobSpec([sys.executable,
  86. 'tools/profiling/latency_profile/profile_analyzer.py',
  87. '--source', '%s.trace' % fnize(line), '--fmt', 'simple',
  88. '--out', 'reports/%s.txt' % fnize(line)], timeout_seconds=None))
  89. cleanup.append(jobset.JobSpec(['rm', '%s.trace' % fnize(line)]))
  90. # periodically flush out the list of jobs: profile_analysis jobs at least
  91. # consume upwards of five gigabytes of ram in some cases, and so analysing
  92. # hundreds of them at once is impractical -- but we want at least some
  93. # concurrency or the work takes too long
  94. if len(benchmarks) >= min(16, multiprocessing.cpu_count()):
  95. # run up to half the cpu count: each benchmark can use up to two cores
  96. # (one for the microbenchmark, one for the data flush)
  97. jobset.run(benchmarks, maxjobs=max(1, multiprocessing.cpu_count()/2))
  98. jobset.run(profile_analysis, maxjobs=multiprocessing.cpu_count())
  99. jobset.run(cleanup, maxjobs=multiprocessing.cpu_count())
  100. benchmarks = []
  101. profile_analysis = []
  102. cleanup = []
  103. # run the remaining benchmarks that weren't flushed
  104. if len(benchmarks):
  105. jobset.run(benchmarks, maxjobs=max(1, multiprocessing.cpu_count()/2))
  106. jobset.run(profile_analysis, maxjobs=multiprocessing.cpu_count())
  107. jobset.run(cleanup, maxjobs=multiprocessing.cpu_count())
  108. def collect_perf(bm_name, args):
  109. """generate flamegraphs"""
  110. heading('Flamegraphs: %s' % bm_name)
  111. subprocess.check_call(
  112. ['make', bm_name,
  113. 'CONFIG=mutrace', '-j', '%d' % multiprocessing.cpu_count()])
  114. benchmarks = []
  115. profile_analysis = []
  116. cleanup = []
  117. for line in subprocess.check_output(['bins/mutrace/%s' % bm_name,
  118. '--benchmark_list_tests']).splitlines():
  119. link(line, '%s.svg' % fnize(line))
  120. benchmarks.append(
  121. jobset.JobSpec(['perf', 'record', '-o', '%s-perf.data' % fnize(line),
  122. '-g', '-F', '997',
  123. 'bins/mutrace/%s' % bm_name,
  124. '--benchmark_filter=^%s$' % line,
  125. '--benchmark_min_time=10']))
  126. profile_analysis.append(
  127. jobset.JobSpec(['tools/run_tests/performance/process_local_perf_flamegraphs.sh'],
  128. environ = {
  129. 'PERF_BASE_NAME': fnize(line),
  130. 'OUTPUT_DIR': 'reports',
  131. 'OUTPUT_FILENAME': fnize(line),
  132. }))
  133. cleanup.append(jobset.JobSpec(['rm', '%s-perf.data' % fnize(line)]))
  134. cleanup.append(jobset.JobSpec(['rm', '%s-out.perf' % fnize(line)]))
  135. # periodically flush out the list of jobs: temporary space required for this
  136. # processing is large
  137. if len(benchmarks) >= 20:
  138. # run up to half the cpu count: each benchmark can use up to two cores
  139. # (one for the microbenchmark, one for the data flush)
  140. jobset.run(benchmarks, maxjobs=1)
  141. jobset.run(profile_analysis, maxjobs=multiprocessing.cpu_count())
  142. jobset.run(cleanup, maxjobs=multiprocessing.cpu_count())
  143. benchmarks = []
  144. profile_analysis = []
  145. cleanup = []
  146. # run the remaining benchmarks that weren't flushed
  147. if len(benchmarks):
  148. jobset.run(benchmarks, maxjobs=1)
  149. jobset.run(profile_analysis, maxjobs=multiprocessing.cpu_count())
  150. jobset.run(cleanup, maxjobs=multiprocessing.cpu_count())
  151. def run_summary(bm_name, cfg, base_json_name):
  152. subprocess.check_call(
  153. ['make', bm_name,
  154. 'CONFIG=%s' % cfg, '-j', '%d' % multiprocessing.cpu_count()])
  155. cmd = ['bins/%s/%s' % (cfg, bm_name),
  156. '--benchmark_out=%s.%s.json' % (base_json_name, cfg),
  157. '--benchmark_out_format=json']
  158. if args.summary_time is not None:
  159. cmd += ['--benchmark_min_time=%d' % args.summary_time]
  160. return subprocess.check_output(cmd)
  161. def collect_summary(bm_name, args):
  162. heading('Summary: %s [no counters]' % bm_name)
  163. text(run_summary(bm_name, 'opt', bm_name))
  164. heading('Summary: %s [with counters]' % bm_name)
  165. text(run_summary(bm_name, 'counters', bm_name))
  166. if args.bigquery_upload:
  167. with open('%s.csv' % bm_name, 'w') as f:
  168. f.write(subprocess.check_output(['tools/profiling/microbenchmarks/bm2bq.py',
  169. '%s.counters.json' % bm_name,
  170. '%s.opt.json' % bm_name]))
  171. subprocess.check_call(['bq', 'load', 'microbenchmarks.microbenchmarks', '%s.csv' % bm_name])
  172. collectors = {
  173. 'latency': collect_latency,
  174. 'perf': collect_perf,
  175. 'summary': collect_summary,
  176. }
  177. argp = argparse.ArgumentParser(description='Collect data from microbenchmarks')
  178. argp.add_argument('-c', '--collect',
  179. choices=sorted(collectors.keys()),
  180. nargs='*',
  181. default=sorted(collectors.keys()),
  182. help='Which collectors should be run against each benchmark')
  183. argp.add_argument('-b', '--benchmarks',
  184. choices=_AVAILABLE_BENCHMARK_TESTS,
  185. default=_AVAILABLE_BENCHMARK_TESTS,
  186. nargs='+',
  187. type=str,
  188. help='Which microbenchmarks should be run')
  189. argp.add_argument('--bigquery_upload',
  190. default=False,
  191. action='store_const',
  192. const=True,
  193. help='Upload results from summary collection to bigquery')
  194. argp.add_argument('--summary_time',
  195. default=None,
  196. type=int,
  197. help='Minimum time to run benchmarks for the summary collection')
  198. args = argp.parse_args()
  199. try:
  200. for collect in args.collect:
  201. for bm_name in args.benchmarks:
  202. collectors[collect](bm_name, args)
  203. finally:
  204. if not os.path.exists('reports'):
  205. os.makedirs('reports')
  206. index_html += "</body>\n</html>\n"
  207. with open('reports/index.html', 'w') as f:
  208. f.write(index_html)