xref: /qemu/scripts/simplebench/simplebench.py (revision 4a44554a65dfc5ccd5dad428981d0ab2959b4b8f)
17cc8e0a5SVladimir Sementsov-Ogievskiy#!/usr/bin/env python
27cc8e0a5SVladimir Sementsov-Ogievskiy#
37cc8e0a5SVladimir Sementsov-Ogievskiy# Simple benchmarking framework
47cc8e0a5SVladimir Sementsov-Ogievskiy#
57cc8e0a5SVladimir Sementsov-Ogievskiy# Copyright (c) 2019 Virtuozzo International GmbH.
67cc8e0a5SVladimir Sementsov-Ogievskiy#
77cc8e0a5SVladimir Sementsov-Ogievskiy# This program is free software; you can redistribute it and/or modify
87cc8e0a5SVladimir Sementsov-Ogievskiy# it under the terms of the GNU General Public License as published by
97cc8e0a5SVladimir Sementsov-Ogievskiy# the Free Software Foundation; either version 2 of the License, or
107cc8e0a5SVladimir Sementsov-Ogievskiy# (at your option) any later version.
117cc8e0a5SVladimir Sementsov-Ogievskiy#
127cc8e0a5SVladimir Sementsov-Ogievskiy# This program is distributed in the hope that it will be useful,
137cc8e0a5SVladimir Sementsov-Ogievskiy# but WITHOUT ANY WARRANTY; without even the implied warranty of
147cc8e0a5SVladimir Sementsov-Ogievskiy# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
157cc8e0a5SVladimir Sementsov-Ogievskiy# GNU General Public License for more details.
167cc8e0a5SVladimir Sementsov-Ogievskiy#
177cc8e0a5SVladimir Sementsov-Ogievskiy# You should have received a copy of the GNU General Public License
187cc8e0a5SVladimir Sementsov-Ogievskiy# along with this program.  If not, see <http://www.gnu.org/licenses/>.
197cc8e0a5SVladimir Sementsov-Ogievskiy#
207cc8e0a5SVladimir Sementsov-Ogievskiy
217cc8e0a5SVladimir Sementsov-Ogievskiy
227cc8e0a5SVladimir Sementsov-Ogievskiydef bench_one(test_func, test_env, test_case, count=5, initial_run=True):
237cc8e0a5SVladimir Sementsov-Ogievskiy    """Benchmark one test-case
247cc8e0a5SVladimir Sementsov-Ogievskiy
257cc8e0a5SVladimir Sementsov-Ogievskiy    test_func   -- benchmarking function with prototype
267cc8e0a5SVladimir Sementsov-Ogievskiy                   test_func(env, case), which takes test_env and test_case
27*4a44554aSVladimir Sementsov-Ogievskiy                   arguments and on success returns dict with 'seconds' or
28*4a44554aSVladimir Sementsov-Ogievskiy                   'iops' (or both) fields, specifying the benchmark result.
29*4a44554aSVladimir Sementsov-Ogievskiy                   If both 'iops' and 'seconds' provided, the 'iops' is
30*4a44554aSVladimir Sementsov-Ogievskiy                   considered the main, and 'seconds' is just an additional
31*4a44554aSVladimir Sementsov-Ogievskiy                   info. On failure test_func should return {'error': str}.
32*4a44554aSVladimir Sementsov-Ogievskiy                   Returned dict may contain any other additional fields.
337cc8e0a5SVladimir Sementsov-Ogievskiy    test_env    -- test environment - opaque first argument for test_func
347cc8e0a5SVladimir Sementsov-Ogievskiy    test_case   -- test case - opaque second argument for test_func
357cc8e0a5SVladimir Sementsov-Ogievskiy    count       -- how many times to call test_func, to calculate average
367cc8e0a5SVladimir Sementsov-Ogievskiy    initial_run -- do initial run of test_func, which don't get into result
377cc8e0a5SVladimir Sementsov-Ogievskiy
387cc8e0a5SVladimir Sementsov-Ogievskiy    Returns dict with the following fields:
397cc8e0a5SVladimir Sementsov-Ogievskiy        'runs':     list of test_func results
40*4a44554aSVladimir Sementsov-Ogievskiy        'dimension': dimension of results, may be 'seconds' or 'iops'
41*4a44554aSVladimir Sementsov-Ogievskiy        'average':  average value (iops or seconds) per run (exists only if at
42*4a44554aSVladimir Sementsov-Ogievskiy                    least one run succeeded)
437cc8e0a5SVladimir Sementsov-Ogievskiy        'delta':    maximum delta between test_func result and the average
447cc8e0a5SVladimir Sementsov-Ogievskiy                    (exists only if at least one run succeeded)
457cc8e0a5SVladimir Sementsov-Ogievskiy        'n-failed': number of failed runs (exists only if at least one run
467cc8e0a5SVladimir Sementsov-Ogievskiy                    failed)
477cc8e0a5SVladimir Sementsov-Ogievskiy    """
487cc8e0a5SVladimir Sementsov-Ogievskiy    if initial_run:
497cc8e0a5SVladimir Sementsov-Ogievskiy        print('  #initial run:')
507cc8e0a5SVladimir Sementsov-Ogievskiy        print('   ', test_func(test_env, test_case))
517cc8e0a5SVladimir Sementsov-Ogievskiy
527cc8e0a5SVladimir Sementsov-Ogievskiy    runs = []
537cc8e0a5SVladimir Sementsov-Ogievskiy    for i in range(count):
547cc8e0a5SVladimir Sementsov-Ogievskiy        print('  #run {}'.format(i+1))
557cc8e0a5SVladimir Sementsov-Ogievskiy        res = test_func(test_env, test_case)
567cc8e0a5SVladimir Sementsov-Ogievskiy        print('   ', res)
577cc8e0a5SVladimir Sementsov-Ogievskiy        runs.append(res)
587cc8e0a5SVladimir Sementsov-Ogievskiy
597cc8e0a5SVladimir Sementsov-Ogievskiy    result = {'runs': runs}
607cc8e0a5SVladimir Sementsov-Ogievskiy
61*4a44554aSVladimir Sementsov-Ogievskiy    succeeded = [r for r in runs if ('seconds' in r or 'iops' in r)]
62270124e7SVladimir Sementsov-Ogievskiy    if succeeded:
63*4a44554aSVladimir Sementsov-Ogievskiy        if 'iops' in succeeded[0]:
64*4a44554aSVladimir Sementsov-Ogievskiy            assert all('iops' in r for r in succeeded)
65*4a44554aSVladimir Sementsov-Ogievskiy            dim = 'iops'
66*4a44554aSVladimir Sementsov-Ogievskiy        else:
67*4a44554aSVladimir Sementsov-Ogievskiy            assert all('seconds' in r for r in succeeded)
68*4a44554aSVladimir Sementsov-Ogievskiy            assert all('iops' not in r for r in succeeded)
69*4a44554aSVladimir Sementsov-Ogievskiy            dim = 'seconds'
70*4a44554aSVladimir Sementsov-Ogievskiy        avg = sum(r[dim] for r in succeeded) / len(succeeded)
71*4a44554aSVladimir Sementsov-Ogievskiy        result['dimension'] = dim
727cc8e0a5SVladimir Sementsov-Ogievskiy        result['average'] = avg
73*4a44554aSVladimir Sementsov-Ogievskiy        result['delta'] = max(abs(r[dim] - avg) for r in succeeded)
747cc8e0a5SVladimir Sementsov-Ogievskiy
75270124e7SVladimir Sementsov-Ogievskiy    if len(succeeded) < count:
76270124e7SVladimir Sementsov-Ogievskiy        result['n-failed'] = count - len(succeeded)
777cc8e0a5SVladimir Sementsov-Ogievskiy
787cc8e0a5SVladimir Sementsov-Ogievskiy    return result
797cc8e0a5SVladimir Sementsov-Ogievskiy
807cc8e0a5SVladimir Sementsov-Ogievskiy
817cc8e0a5SVladimir Sementsov-Ogievskiydef ascii_one(result):
827cc8e0a5SVladimir Sementsov-Ogievskiy    """Return ASCII representation of bench_one() returned dict."""
837cc8e0a5SVladimir Sementsov-Ogievskiy    if 'average' in result:
847cc8e0a5SVladimir Sementsov-Ogievskiy        s = '{:.2f} +- {:.2f}'.format(result['average'], result['delta'])
857cc8e0a5SVladimir Sementsov-Ogievskiy        if 'n-failed' in result:
867cc8e0a5SVladimir Sementsov-Ogievskiy            s += '\n({} failed)'.format(result['n-failed'])
877cc8e0a5SVladimir Sementsov-Ogievskiy        return s
887cc8e0a5SVladimir Sementsov-Ogievskiy    else:
897cc8e0a5SVladimir Sementsov-Ogievskiy        return 'FAILED'
907cc8e0a5SVladimir Sementsov-Ogievskiy
917cc8e0a5SVladimir Sementsov-Ogievskiy
927cc8e0a5SVladimir Sementsov-Ogievskiydef bench(test_func, test_envs, test_cases, *args, **vargs):
937cc8e0a5SVladimir Sementsov-Ogievskiy    """Fill benchmark table
947cc8e0a5SVladimir Sementsov-Ogievskiy
957cc8e0a5SVladimir Sementsov-Ogievskiy    test_func -- benchmarking function, see bench_one for description
967cc8e0a5SVladimir Sementsov-Ogievskiy    test_envs -- list of test environments, see bench_one
977cc8e0a5SVladimir Sementsov-Ogievskiy    test_cases -- list of test cases, see bench_one
987cc8e0a5SVladimir Sementsov-Ogievskiy    args, vargs -- additional arguments for bench_one
997cc8e0a5SVladimir Sementsov-Ogievskiy
1007cc8e0a5SVladimir Sementsov-Ogievskiy    Returns dict with the following fields:
1017cc8e0a5SVladimir Sementsov-Ogievskiy        'envs':  test_envs
1027cc8e0a5SVladimir Sementsov-Ogievskiy        'cases': test_cases
1037cc8e0a5SVladimir Sementsov-Ogievskiy        'tab':   filled 2D array, where cell [i][j] is bench_one result for
1047cc8e0a5SVladimir Sementsov-Ogievskiy                 test_cases[i] for test_envs[j] (i.e., rows are test cases and
1057cc8e0a5SVladimir Sementsov-Ogievskiy                 columns are test environments)
1067cc8e0a5SVladimir Sementsov-Ogievskiy    """
1077cc8e0a5SVladimir Sementsov-Ogievskiy    tab = {}
1087cc8e0a5SVladimir Sementsov-Ogievskiy    results = {
1097cc8e0a5SVladimir Sementsov-Ogievskiy        'envs': test_envs,
1107cc8e0a5SVladimir Sementsov-Ogievskiy        'cases': test_cases,
1117cc8e0a5SVladimir Sementsov-Ogievskiy        'tab': tab
1127cc8e0a5SVladimir Sementsov-Ogievskiy    }
1137cc8e0a5SVladimir Sementsov-Ogievskiy    n = 1
1147cc8e0a5SVladimir Sementsov-Ogievskiy    n_tests = len(test_envs) * len(test_cases)
1157cc8e0a5SVladimir Sementsov-Ogievskiy    for env in test_envs:
1167cc8e0a5SVladimir Sementsov-Ogievskiy        for case in test_cases:
1177cc8e0a5SVladimir Sementsov-Ogievskiy            print('Testing {}/{}: {} :: {}'.format(n, n_tests,
1187cc8e0a5SVladimir Sementsov-Ogievskiy                                                   env['id'], case['id']))
1197cc8e0a5SVladimir Sementsov-Ogievskiy            if case['id'] not in tab:
1207cc8e0a5SVladimir Sementsov-Ogievskiy                tab[case['id']] = {}
1217cc8e0a5SVladimir Sementsov-Ogievskiy            tab[case['id']][env['id']] = bench_one(test_func, env, case,
1227cc8e0a5SVladimir Sementsov-Ogievskiy                                                   *args, **vargs)
1237cc8e0a5SVladimir Sementsov-Ogievskiy            n += 1
1247cc8e0a5SVladimir Sementsov-Ogievskiy
1257cc8e0a5SVladimir Sementsov-Ogievskiy    print('Done')
1267cc8e0a5SVladimir Sementsov-Ogievskiy    return results
1277cc8e0a5SVladimir Sementsov-Ogievskiy
1287cc8e0a5SVladimir Sementsov-Ogievskiy
1297cc8e0a5SVladimir Sementsov-Ogievskiydef ascii(results):
1307cc8e0a5SVladimir Sementsov-Ogievskiy    """Return ASCII representation of bench() returned dict."""
1317cc8e0a5SVladimir Sementsov-Ogievskiy    from tabulate import tabulate
1327cc8e0a5SVladimir Sementsov-Ogievskiy
133*4a44554aSVladimir Sementsov-Ogievskiy    dim = None
1347cc8e0a5SVladimir Sementsov-Ogievskiy    tab = [[""] + [c['id'] for c in results['envs']]]
1357cc8e0a5SVladimir Sementsov-Ogievskiy    for case in results['cases']:
1367cc8e0a5SVladimir Sementsov-Ogievskiy        row = [case['id']]
1377cc8e0a5SVladimir Sementsov-Ogievskiy        for env in results['envs']:
138*4a44554aSVladimir Sementsov-Ogievskiy            res = results['tab'][case['id']][env['id']]
139*4a44554aSVladimir Sementsov-Ogievskiy            if dim is None:
140*4a44554aSVladimir Sementsov-Ogievskiy                dim = res['dimension']
141*4a44554aSVladimir Sementsov-Ogievskiy            else:
142*4a44554aSVladimir Sementsov-Ogievskiy                assert dim == res['dimension']
143*4a44554aSVladimir Sementsov-Ogievskiy            row.append(ascii_one(res))
1447cc8e0a5SVladimir Sementsov-Ogievskiy        tab.append(row)
1457cc8e0a5SVladimir Sementsov-Ogievskiy
146*4a44554aSVladimir Sementsov-Ogievskiy    return f'All results are in {dim}\n\n' + tabulate(tab)
147