1*7cc8e0a5SVladimir Sementsov-Ogievskiy#!/usr/bin/env python 2*7cc8e0a5SVladimir Sementsov-Ogievskiy# 3*7cc8e0a5SVladimir Sementsov-Ogievskiy# Simple benchmarking framework 4*7cc8e0a5SVladimir Sementsov-Ogievskiy# 5*7cc8e0a5SVladimir Sementsov-Ogievskiy# Copyright (c) 2019 Virtuozzo International GmbH. 6*7cc8e0a5SVladimir Sementsov-Ogievskiy# 7*7cc8e0a5SVladimir Sementsov-Ogievskiy# This program is free software; you can redistribute it and/or modify 8*7cc8e0a5SVladimir Sementsov-Ogievskiy# it under the terms of the GNU General Public License as published by 9*7cc8e0a5SVladimir Sementsov-Ogievskiy# the Free Software Foundation; either version 2 of the License, or 10*7cc8e0a5SVladimir Sementsov-Ogievskiy# (at your option) any later version. 11*7cc8e0a5SVladimir Sementsov-Ogievskiy# 12*7cc8e0a5SVladimir Sementsov-Ogievskiy# This program is distributed in the hope that it will be useful, 13*7cc8e0a5SVladimir Sementsov-Ogievskiy# but WITHOUT ANY WARRANTY; without even the implied warranty of 14*7cc8e0a5SVladimir Sementsov-Ogievskiy# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15*7cc8e0a5SVladimir Sementsov-Ogievskiy# GNU General Public License for more details. 16*7cc8e0a5SVladimir Sementsov-Ogievskiy# 17*7cc8e0a5SVladimir Sementsov-Ogievskiy# You should have received a copy of the GNU General Public License 18*7cc8e0a5SVladimir Sementsov-Ogievskiy# along with this program. If not, see <http://www.gnu.org/licenses/>. 19*7cc8e0a5SVladimir Sementsov-Ogievskiy# 20*7cc8e0a5SVladimir Sementsov-Ogievskiy 21*7cc8e0a5SVladimir Sementsov-Ogievskiy 22*7cc8e0a5SVladimir Sementsov-Ogievskiydef bench_one(test_func, test_env, test_case, count=5, initial_run=True): 23*7cc8e0a5SVladimir Sementsov-Ogievskiy """Benchmark one test-case 24*7cc8e0a5SVladimir Sementsov-Ogievskiy 25*7cc8e0a5SVladimir Sementsov-Ogievskiy test_func -- benchmarking function with prototype 26*7cc8e0a5SVladimir Sementsov-Ogievskiy test_func(env, case), which takes test_env and test_case 27*7cc8e0a5SVladimir Sementsov-Ogievskiy arguments and returns {'seconds': int} (which is benchmark 28*7cc8e0a5SVladimir Sementsov-Ogievskiy result) on success and {'error': str} on error. Returned 29*7cc8e0a5SVladimir Sementsov-Ogievskiy dict may contain any other additional fields. 30*7cc8e0a5SVladimir Sementsov-Ogievskiy test_env -- test environment - opaque first argument for test_func 31*7cc8e0a5SVladimir Sementsov-Ogievskiy test_case -- test case - opaque second argument for test_func 32*7cc8e0a5SVladimir Sementsov-Ogievskiy count -- how many times to call test_func, to calculate average 33*7cc8e0a5SVladimir Sementsov-Ogievskiy initial_run -- do initial run of test_func, which don't get into result 34*7cc8e0a5SVladimir Sementsov-Ogievskiy 35*7cc8e0a5SVladimir Sementsov-Ogievskiy Returns dict with the following fields: 36*7cc8e0a5SVladimir Sementsov-Ogievskiy 'runs': list of test_func results 37*7cc8e0a5SVladimir Sementsov-Ogievskiy 'average': average seconds per run (exists only if at least one run 38*7cc8e0a5SVladimir Sementsov-Ogievskiy succeeded) 39*7cc8e0a5SVladimir Sementsov-Ogievskiy 'delta': maximum delta between test_func result and the average 40*7cc8e0a5SVladimir Sementsov-Ogievskiy (exists only if at least one run succeeded) 41*7cc8e0a5SVladimir Sementsov-Ogievskiy 'n-failed': number of failed runs (exists only if at least one run 42*7cc8e0a5SVladimir Sementsov-Ogievskiy failed) 43*7cc8e0a5SVladimir Sementsov-Ogievskiy """ 44*7cc8e0a5SVladimir Sementsov-Ogievskiy if initial_run: 45*7cc8e0a5SVladimir Sementsov-Ogievskiy print(' #initial run:') 46*7cc8e0a5SVladimir Sementsov-Ogievskiy print(' ', test_func(test_env, test_case)) 47*7cc8e0a5SVladimir Sementsov-Ogievskiy 48*7cc8e0a5SVladimir Sementsov-Ogievskiy runs = [] 49*7cc8e0a5SVladimir Sementsov-Ogievskiy for i in range(count): 50*7cc8e0a5SVladimir Sementsov-Ogievskiy print(' #run {}'.format(i+1)) 51*7cc8e0a5SVladimir Sementsov-Ogievskiy res = test_func(test_env, test_case) 52*7cc8e0a5SVladimir Sementsov-Ogievskiy print(' ', res) 53*7cc8e0a5SVladimir Sementsov-Ogievskiy runs.append(res) 54*7cc8e0a5SVladimir Sementsov-Ogievskiy 55*7cc8e0a5SVladimir Sementsov-Ogievskiy result = {'runs': runs} 56*7cc8e0a5SVladimir Sementsov-Ogievskiy 57*7cc8e0a5SVladimir Sementsov-Ogievskiy successed = [r for r in runs if ('seconds' in r)] 58*7cc8e0a5SVladimir Sementsov-Ogievskiy if successed: 59*7cc8e0a5SVladimir Sementsov-Ogievskiy avg = sum(r['seconds'] for r in successed) / len(successed) 60*7cc8e0a5SVladimir Sementsov-Ogievskiy result['average'] = avg 61*7cc8e0a5SVladimir Sementsov-Ogievskiy result['delta'] = max(abs(r['seconds'] - avg) for r in successed) 62*7cc8e0a5SVladimir Sementsov-Ogievskiy 63*7cc8e0a5SVladimir Sementsov-Ogievskiy if len(successed) < count: 64*7cc8e0a5SVladimir Sementsov-Ogievskiy result['n-failed'] = count - len(successed) 65*7cc8e0a5SVladimir Sementsov-Ogievskiy 66*7cc8e0a5SVladimir Sementsov-Ogievskiy return result 67*7cc8e0a5SVladimir Sementsov-Ogievskiy 68*7cc8e0a5SVladimir Sementsov-Ogievskiy 69*7cc8e0a5SVladimir Sementsov-Ogievskiydef ascii_one(result): 70*7cc8e0a5SVladimir Sementsov-Ogievskiy """Return ASCII representation of bench_one() returned dict.""" 71*7cc8e0a5SVladimir Sementsov-Ogievskiy if 'average' in result: 72*7cc8e0a5SVladimir Sementsov-Ogievskiy s = '{:.2f} +- {:.2f}'.format(result['average'], result['delta']) 73*7cc8e0a5SVladimir Sementsov-Ogievskiy if 'n-failed' in result: 74*7cc8e0a5SVladimir Sementsov-Ogievskiy s += '\n({} failed)'.format(result['n-failed']) 75*7cc8e0a5SVladimir Sementsov-Ogievskiy return s 76*7cc8e0a5SVladimir Sementsov-Ogievskiy else: 77*7cc8e0a5SVladimir Sementsov-Ogievskiy return 'FAILED' 78*7cc8e0a5SVladimir Sementsov-Ogievskiy 79*7cc8e0a5SVladimir Sementsov-Ogievskiy 80*7cc8e0a5SVladimir Sementsov-Ogievskiydef bench(test_func, test_envs, test_cases, *args, **vargs): 81*7cc8e0a5SVladimir Sementsov-Ogievskiy """Fill benchmark table 82*7cc8e0a5SVladimir Sementsov-Ogievskiy 83*7cc8e0a5SVladimir Sementsov-Ogievskiy test_func -- benchmarking function, see bench_one for description 84*7cc8e0a5SVladimir Sementsov-Ogievskiy test_envs -- list of test environments, see bench_one 85*7cc8e0a5SVladimir Sementsov-Ogievskiy test_cases -- list of test cases, see bench_one 86*7cc8e0a5SVladimir Sementsov-Ogievskiy args, vargs -- additional arguments for bench_one 87*7cc8e0a5SVladimir Sementsov-Ogievskiy 88*7cc8e0a5SVladimir Sementsov-Ogievskiy Returns dict with the following fields: 89*7cc8e0a5SVladimir Sementsov-Ogievskiy 'envs': test_envs 90*7cc8e0a5SVladimir Sementsov-Ogievskiy 'cases': test_cases 91*7cc8e0a5SVladimir Sementsov-Ogievskiy 'tab': filled 2D array, where cell [i][j] is bench_one result for 92*7cc8e0a5SVladimir Sementsov-Ogievskiy test_cases[i] for test_envs[j] (i.e., rows are test cases and 93*7cc8e0a5SVladimir Sementsov-Ogievskiy columns are test environments) 94*7cc8e0a5SVladimir Sementsov-Ogievskiy """ 95*7cc8e0a5SVladimir Sementsov-Ogievskiy tab = {} 96*7cc8e0a5SVladimir Sementsov-Ogievskiy results = { 97*7cc8e0a5SVladimir Sementsov-Ogievskiy 'envs': test_envs, 98*7cc8e0a5SVladimir Sementsov-Ogievskiy 'cases': test_cases, 99*7cc8e0a5SVladimir Sementsov-Ogievskiy 'tab': tab 100*7cc8e0a5SVladimir Sementsov-Ogievskiy } 101*7cc8e0a5SVladimir Sementsov-Ogievskiy n = 1 102*7cc8e0a5SVladimir Sementsov-Ogievskiy n_tests = len(test_envs) * len(test_cases) 103*7cc8e0a5SVladimir Sementsov-Ogievskiy for env in test_envs: 104*7cc8e0a5SVladimir Sementsov-Ogievskiy for case in test_cases: 105*7cc8e0a5SVladimir Sementsov-Ogievskiy print('Testing {}/{}: {} :: {}'.format(n, n_tests, 106*7cc8e0a5SVladimir Sementsov-Ogievskiy env['id'], case['id'])) 107*7cc8e0a5SVladimir Sementsov-Ogievskiy if case['id'] not in tab: 108*7cc8e0a5SVladimir Sementsov-Ogievskiy tab[case['id']] = {} 109*7cc8e0a5SVladimir Sementsov-Ogievskiy tab[case['id']][env['id']] = bench_one(test_func, env, case, 110*7cc8e0a5SVladimir Sementsov-Ogievskiy *args, **vargs) 111*7cc8e0a5SVladimir Sementsov-Ogievskiy n += 1 112*7cc8e0a5SVladimir Sementsov-Ogievskiy 113*7cc8e0a5SVladimir Sementsov-Ogievskiy print('Done') 114*7cc8e0a5SVladimir Sementsov-Ogievskiy return results 115*7cc8e0a5SVladimir Sementsov-Ogievskiy 116*7cc8e0a5SVladimir Sementsov-Ogievskiy 117*7cc8e0a5SVladimir Sementsov-Ogievskiydef ascii(results): 118*7cc8e0a5SVladimir Sementsov-Ogievskiy """Return ASCII representation of bench() returned dict.""" 119*7cc8e0a5SVladimir Sementsov-Ogievskiy from tabulate import tabulate 120*7cc8e0a5SVladimir Sementsov-Ogievskiy 121*7cc8e0a5SVladimir Sementsov-Ogievskiy tab = [[""] + [c['id'] for c in results['envs']]] 122*7cc8e0a5SVladimir Sementsov-Ogievskiy for case in results['cases']: 123*7cc8e0a5SVladimir Sementsov-Ogievskiy row = [case['id']] 124*7cc8e0a5SVladimir Sementsov-Ogievskiy for env in results['envs']: 125*7cc8e0a5SVladimir Sementsov-Ogievskiy row.append(ascii_one(results['tab'][case['id']][env['id']])) 126*7cc8e0a5SVladimir Sementsov-Ogievskiy tab.append(row) 127*7cc8e0a5SVladimir Sementsov-Ogievskiy 128*7cc8e0a5SVladimir Sementsov-Ogievskiy return tabulate(tab) 129