simplebench.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env python
  2. #
  3. # Simple benchmarking framework
  4. #
  5. # Copyright (c) 2019 Virtuozzo International GmbH.
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. import statistics
  21. import subprocess
  22. import time
  23. def do_drop_caches():
  24. subprocess.run('sync; echo 3 > /proc/sys/vm/drop_caches', shell=True,
  25. check=True)
  26. def bench_one(test_func, test_env, test_case, count=5, initial_run=True,
  27. slow_limit=100, drop_caches=False):
  28. """Benchmark one test-case
  29. test_func -- benchmarking function with prototype
  30. test_func(env, case), which takes test_env and test_case
  31. arguments and on success returns dict with 'seconds' or
  32. 'iops' (or both) fields, specifying the benchmark result.
  33. If both 'iops' and 'seconds' provided, the 'iops' is
  34. considered the main, and 'seconds' is just an additional
  35. info. On failure test_func should return {'error': str}.
  36. Returned dict may contain any other additional fields.
  37. test_env -- test environment - opaque first argument for test_func
  38. test_case -- test case - opaque second argument for test_func
  39. count -- how many times to call test_func, to calculate average
  40. initial_run -- do initial run of test_func, which don't get into result
  41. slow_limit -- stop at slow run (that exceedes the slow_limit by seconds).
  42. (initial run is not measured)
  43. drop_caches -- drop caches before each run
  44. Returns dict with the following fields:
  45. 'runs': list of test_func results
  46. 'dimension': dimension of results, may be 'seconds' or 'iops'
  47. 'average': average value (iops or seconds) per run (exists only if at
  48. least one run succeeded)
  49. 'stdev': standard deviation of results
  50. (exists only if at least one run succeeded)
  51. 'n-failed': number of failed runs (exists only if at least one run
  52. failed)
  53. """
  54. if initial_run:
  55. print(' #initial run:')
  56. do_drop_caches()
  57. print(' ', test_func(test_env, test_case))
  58. runs = []
  59. for i in range(count):
  60. t = time.time()
  61. print(' #run {}'.format(i+1))
  62. do_drop_caches()
  63. res = test_func(test_env, test_case)
  64. print(' ', res)
  65. runs.append(res)
  66. if time.time() - t > slow_limit:
  67. print(' - run is too slow, stop here')
  68. break
  69. count = len(runs)
  70. result = {'runs': runs}
  71. succeeded = [r for r in runs if ('seconds' in r or 'iops' in r)]
  72. if succeeded:
  73. if 'iops' in succeeded[0]:
  74. assert all('iops' in r for r in succeeded)
  75. dim = 'iops'
  76. else:
  77. assert all('seconds' in r for r in succeeded)
  78. assert all('iops' not in r for r in succeeded)
  79. dim = 'seconds'
  80. result['dimension'] = dim
  81. result['average'] = statistics.mean(r[dim] for r in succeeded)
  82. if len(succeeded) == 1:
  83. result['stdev'] = 0
  84. else:
  85. result['stdev'] = statistics.stdev(r[dim] for r in succeeded)
  86. if len(succeeded) < count:
  87. result['n-failed'] = count - len(succeeded)
  88. return result
  89. def bench(test_func, test_envs, test_cases, *args, **vargs):
  90. """Fill benchmark table
  91. test_func -- benchmarking function, see bench_one for description
  92. test_envs -- list of test environments, see bench_one
  93. test_cases -- list of test cases, see bench_one
  94. args, vargs -- additional arguments for bench_one
  95. Returns dict with the following fields:
  96. 'envs': test_envs
  97. 'cases': test_cases
  98. 'tab': filled 2D array, where cell [i][j] is bench_one result for
  99. test_cases[i] for test_envs[j] (i.e., rows are test cases and
  100. columns are test environments)
  101. """
  102. tab = {}
  103. results = {
  104. 'envs': test_envs,
  105. 'cases': test_cases,
  106. 'tab': tab
  107. }
  108. n = 1
  109. n_tests = len(test_envs) * len(test_cases)
  110. for env in test_envs:
  111. for case in test_cases:
  112. print('Testing {}/{}: {} :: {}'.format(n, n_tests,
  113. env['id'], case['id']))
  114. if case['id'] not in tab:
  115. tab[case['id']] = {}
  116. tab[case['id']][env['id']] = bench_one(test_func, env, case,
  117. *args, **vargs)
  118. n += 1
  119. print('Done')
  120. return results