From 28f36868c6db0ef4a5aaaa953e9dd156edf81ea0 Mon Sep 17 00:00:00 2001 From: Arnaud Fontaine <arnaud.fontaine@nexedi.com> Date: Fri, 15 Apr 2011 12:41:33 +0000 Subject: [PATCH] Add erp5.utils.benchmark git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk/utils@45502 20353a03-c40f-0410-a6d1-a30d3c3de9de --- erp5/util/benchmark/__init__.py | 0 erp5/util/benchmark/benchmark.py | 361 ++++++++++++++++++++++++++ erp5/util/benchmark/generateReport.py | 278 ++++++++++++++++++++ erp5/util/benchmark/runBenchmark.py | 192 ++++++++++++++ 4 files changed, 831 insertions(+) create mode 100644 erp5/util/benchmark/__init__.py create mode 100644 erp5/util/benchmark/benchmark.py create mode 100755 erp5/util/benchmark/generateReport.py create mode 100755 erp5/util/benchmark/runBenchmark.py diff --git a/erp5/util/benchmark/__init__.py b/erp5/util/benchmark/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erp5/util/benchmark/benchmark.py b/erp5/util/benchmark/benchmark.py new file mode 100644 index 0000000000..9b8d9e2f8f --- /dev/null +++ b/erp5/util/benchmark/benchmark.py @@ -0,0 +1,361 @@ +############################################################################## +# +# Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved. +# Arnaud Fontaine <arnaud.fontaine@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import argparse +import functools + +class ArgumentType(object): + @classmethod + def directoryType(cls, path): + if not (os.path.isdir(path) and os.access(path, os.W_OK)): + raise argparse.ArgumentTypeError("'%s' is not a valid directory or is "\ + "not writable" % path) + + return path + + @classmethod + def objectFromModule(cls, module_name, object_name=None, callable_object=False): + if module_name.endswith('.py'): + module_name = module_name[:-3] + + if not object_name: + object_name = module_name + + import sys + sys.path.append(os.getcwd()) + + try: + module = __import__(module_name, globals(), locals(), [object_name], -1) + except Exception, e: + raise argparse.ArgumentTypeError("Cannot import '%s.%s': %s" % \ + (module_name, object_name, str(e))) + + try: + obj = getattr(module, object_name) + except AttributeError: + raise argparse.ArgumentTypeError("Could not get '%s' in '%s'" % \ + (object_name, module_name)) + + if callable_object and not callable(obj): + raise argparse.ArgumentTypeError( + "'%s.%s' is not callable" % (module_name, object_name)) + + return obj + + @classmethod + def strictlyPositiveIntType(cls, value): + try: + converted_value = int(value) + except ValueError: + pass + else: + if converted_value > 0: + return converted_value + + raise argparse.ArgumentTypeError('expects a strictly positive integer') + + @classmethod + def strictlyPositiveIntOrRangeType(cls, value): + try: + return cls.strictlyPositiveIntType(value) + except argparse.ArgumentTypeError: + try: + min_max_list = value.split(',') + except ValueError: + pass + else: + if len(min_max_list) == 2: + minimum, maximum = cls.strictlyPositiveIntType(min_max_list[0]), \ + cls.strictlyPositiveIntType(min_max_list[1]) + + if minimum >= maximum: + raise argparse.ArgumentTypeError('%d >= %d' % (minimum, maximum)) + + return (minimum, maximum) + + raise argparse.ArgumentTypeError( + 'expects either a strictly positive integer or a range of strictly ' + 'positive integer separated by a comma') + + @classmethod + def ERP5UrlType(cls, url): + if url[-1] == '/': + url_list = url.rsplit('/', 2)[:-1] + else: + url_list = url.rsplit('/', 1) + + url_list[0] = url_list[0] + '/' + if len(url_list) != 2: + raise argparse.ArgumentTypeError("Invalid URL given") + + return url_list + +import sys +import math + +class BenchmarkResultStatistic(object): + def __init__(self, suite, label): + self.suite = suite + self.label = label + + self.full_label = '%s: %s' % (self.suite, self.label) + + self.minimum = sys.maxint + self.maximum = -1 + self.n = 0 + self.error_sum = 0 + + # For calculating the mean + self._value_sum = 0 + + # For calculating the standard deviation + self._variance_sum = 0 + self._mean = 0 + + def add(self, value): + if value == 0: + self.error_sum += 1 + return + + if value < self.minimum: + self.minimum = value + if value > self.maximum: + self.maximum = value + + self._value_sum += value + self.n += 1 + + delta = value - self._mean + self._mean += delta / self.n + self._variance_sum += delta * (value - self._mean) + + @property + def mean(self): + return self._value_sum / self.n + + @property + def standard_deviation(self): + return math.sqrt(self._variance_sum / self.n) + +class BenchmarkResult(object): + def __init__(self): + self._stat_list = [] + self._suite_idx = 0 + self._result_idx = 0 + self._result_list = [] + self._first_iteration = True + self._current_suite_name = None + self._result_idx_checkpoint_list = [] + + def enterSuite(self, name): + self._current_suite_name = name + + def __call__(self, label, value): + self._result_list.append(value) + if self._first_iteration: + self._stat_list.append(BenchmarkResultStatistic(self._current_suite_name, + label)) + + self._stat_list[self._result_idx].add(value) + self._result_idx += 1 + + def exitSuite(self): + if self._first_iteration: + self._result_idx_checkpoint_list.append(self._result_idx) + else: + expected_result_idx = self._result_idx_checkpoint_list[self._suite_idx] + while self._result_idx != expected_result_idx: + self._result_list.append(0) + self._stat_list[self._result_idx].add(0) + self._result_idx += 1 + + self._suite_idx += 1 + + def getLabelList(self): + self._first_iteration = False + return [ stat.full_label for stat in self._stat_list ] + + def getResultList(self): + self._suite_idx = 0 + self._result_idx = 0 + + result_list = self._result_list + self._result_list = [] + return result_list + + def getStatList(self): + return self._stat_list + + def getCurrentSuiteStatList(self): + start_index = self._suite_idx and \ + self._result_idx_checkpoint_list[self._suite_idx - 1] or 0 + + return self._stat_list[start_index:self._result_idx] + +import multiprocessing +import csv +import traceback +import os +import logging +import signal +import sys + +from erp5.utils.test_browser.browser import Browser + +class BenchmarkProcess(multiprocessing.Process): + def __init__(self, exit_msg_queue, nb_users, user_index, + argument_namespace, *args, **kwargs): + self._exit_msg_queue = exit_msg_queue + self._nb_users = nb_users + self._user_index = user_index + self._argument_namespace = argument_namespace + + filename_path_prefix = self.getFilenamePrefix() + self._result_filename = "%s.csv" % filename_path_prefix + self._log_filename = "%s.log" % filename_path_prefix + + # Initialized when running the test + self._csv_writer = None + self._browser = None + + self._current_repeat = 1 + self._current_result = BenchmarkResult() + + super(BenchmarkProcess, self).__init__(*args, **kwargs) + + def getFilenamePrefix(self): + max_nb_users = isinstance(self._argument_namespace.users, int) and \ + self._argument_namespace.users or self._argument_namespace.users[1] + + fmt = "%%s-%%drepeat-%%0%ddusers-process%%0%dd" % \ + (len(str(max_nb_users)), len(str(self._nb_users))) + + return os.path.join( + self._argument_namespace.report_directory, + fmt % (self._argument_namespace.filename_prefix, + self._argument_namespace.repeat, + self._nb_users, + self._user_index)) + + def stopGracefully(self, *args, **kwargs): + raise StopIteration, "Interrupted by user" + + def getBrowser(self): + info_list = tuple(self._argument_namespace.url) + \ + tuple(self._argument_namespace.user_tuple[self._user_index]) + + return Browser(*info_list, + is_debug=self._argument_namespace.is_debug, + log_filename=self._log_filename, + is_legacy_listbox=self._argument_namespace.is_legacy_listbox) + + def runBenchmarkSuiteList(self): + for target_idx, target in enumerate(self._argument_namespace.benchmark_suite_list): + self._logger.debug("EXECUTE: %s" % target) + self._current_result.enterSuite(target.__name__) + + try: + target(self._current_result, self._browser) + except: + msg = "%s: %s" % (target, traceback.format_exc()) + if self._current_repeat == 1: + self._logger.error(msg) + raise + + self._logger.warning(msg) + + for stat in self._current_result.getCurrentSuiteStatList(): + mean = stat.mean + + self._logger.info("%s: min=%.3f, mean=%.3f (+/- %.3f), max=%.3f" % \ + (stat.full_label, + stat.minimum, + mean, + stat.standard_deviation, + stat.maximum)) + + if self._argument_namespace.max_global_average and \ + mean > self._argument_namespace.max_global_average: + self._logger.info("Stopping as mean is greater than maximum " + "global average") + + raise StopIteration, "See: %s" % self._log_filename + + self._current_result.exitSuite() + + if self._current_repeat == 1: + self._csv_writer.writerow(self._current_result.getLabelList()) + + result_list = self._current_result.getResultList() + self._logger.debug("RESULTS: %s" % result_list) + self._csv_writer.writerow(result_list) + + def getLogger(self): + logging.basicConfig(filename=self._log_filename, filemode='w', + level=self._argument_namespace.is_debug and \ + logging.DEBUG or logging.INFO) + + return logging.getLogger('erp5.utils.benchmark') + + def run(self): + self._logger = self.getLogger() + + if self._argument_namespace.repeat != -1: + signal.signal(signal.SIGTERM, self.stopGracefully) + + try: + self._browser = self.getBrowser() + except: + self._logger.error(traceback.format_exc()) + raise + + exit_status = 0 + exit_msg = None + + # Create the result CSV file + with open(self._result_filename, 'wb') as result_file: + self._csv_writer = csv.writer(result_file, delimiter=',', + quoting=csv.QUOTE_MINIMAL) + + try: + while self._current_repeat != (self._argument_namespace.repeat + 1): + self._logger.info("Iteration: %d" % self._current_repeat) + self.runBenchmarkSuiteList() + self._current_repeat += 1 + + except StopIteration, e: + exit_msg = str(e) + exit_status = 1 + + except: + self._logger.error(traceback.format_exc()) + exit_msg = "An error occured, see: %s" % self._log_filename + exit_status = 2 + + self._exit_msg_queue.put(exit_msg) + sys.exit(exit_status) diff --git a/erp5/util/benchmark/generateReport.py b/erp5/util/benchmark/generateReport.py new file mode 100755 index 0000000000..2e3071e4d9 --- /dev/null +++ b/erp5/util/benchmark/generateReport.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################## +# +# Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved. +# Arnaud Fontaine <arnaud.fontaine@nexedi.com> +# +# First version: ERP5Mechanize from Vincent Pelletier <vincent@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import argparse + +def parseArguments(): + parser = argparse.ArgumentParser( + description='Generate reports for ERP5 benchmarking suites.') + + parser.add_argument('--enable-debug', + dest='is_debug', + action='store_true', + default=False, + help='Enable debug messages') + + parser.add_argument('--filename-prefix', + default='result', + metavar='PREFIX', + help='Filename prefix for results CSV files ' + '(default: result)') + + parser.add_argument('--output-filename', + default='results.pdf', + metavar='FILENAME', + help='PDF output file (default: results.pdf)') + + parser.add_argument('report_directory', + help='Reports directory') + + namespace = parser.parse_args() + + return namespace + +import csv + +from benchmark import BenchmarkResultStatistic + +def computeStatisticFromFilenameList(argument_namespace, filename_list): + reader_list = [] + stat_list = [] + label_list = [] + + for filename in filename_list: + reader = csv.reader(open(filename, 'rb'), delimiter=',', + quoting=csv.QUOTE_MINIMAL) + + reader_list.append(reader) + + # Get headers + row_list = reader.next() + if not label_list: + label_list = row_list + for label in label_list: + stat_list.append(BenchmarkResultStatistic(*label.split(': ', 1))) + + if row_list != label_list: + raise AssertionError, "ERROR: Result labels: %s != %s" % \ + (label_list, row_list) + + for row_list in reader: + for idx, row in enumerate(row_list): + stat_list[idx].add(float(row)) + + return stat_list + +def formatFloatList(value_list): + return [ format(value, ".3f") for value in value_list ] + +import numpy +import pylab + +from matplotlib import pyplot, ticker + +def drawBarDiagram(pdf, title, stat_list): + mean_list = [] + yerr_list = [] + minimum_list = [] + maximum_list = [] + label_list = [] + error_list = [] + + for stat in stat_list: + mean_list.append(stat.mean) + yerr_list.append(stat.standard_deviation) + minimum_list.append(stat.minimum) + maximum_list.append(stat.maximum) + label_list.append(stat.label) + error_list.append(stat.error_sum) + + min_array = numpy.array(minimum_list) + mean_array = numpy.array(mean_list) + max_array = numpy.array(maximum_list) + + yerr_lower = numpy.minimum(mean_array - min_array, yerr_list) + yerr_upper = numpy.minimum(max_array - mean_array, yerr_list) + + ## Draw diagrams + # Create the figure + figure = pyplot.figure(figsize=(11.69, 8.29)) + figure.subplots_adjust(bottom=0.13, right=0.98, top=0.95) + pyplot.title(title) + + # Create the axes along with their labels + axes = figure.add_subplot(111) + axes.set_ylabel('Seconds') + axes.set_xticks([]) + + axes.yaxis.set_major_locator(ticker.MultipleLocator(0.5)) + axes.yaxis.set_minor_locator(ticker.MultipleLocator(0.25)) + axes.yaxis.grid(True, 'major', linewidth=1.5) + axes.yaxis.grid(True, 'minor') + + # Create the bars + ind = numpy.arange(len(label_list)) + width = 0.33 + + min_rects = axes.bar(ind, minimum_list, width, color='y', label='Minimum') + + avg_rects = axes.bar(ind + width, mean_list, width, color='r', label='Mean') + + axes.errorbar(numpy.arange(0.5, len(stat_list)), mean_list, + yerr=[yerr_lower, yerr_upper], fmt=None, + label='Standard deviation') + + max_rects = axes.bar(ind + width * 2, maximum_list, width, label='Maximum', + color='g') + + # Add the legend of bars + axes.legend(loc=0) + + axes.table(rowLabels=['Minimum', 'Average', 'Std. deviation', 'Maximum', 'Errors'], + colLabels=label_list, + cellText=[formatFloatList(minimum_list), + formatFloatList(mean_list), + formatFloatList(yerr_list), + formatFloatList(maximum_list), + error_list], + rowColours=('y', 'r', 'b', 'g', 'w'), + loc='bottom', + colLoc='center', + rowLoc='center', + cellLoc='center') + + pdf.savefig() + pylab.close() + +def drawConcurrentUsersPlot(pdf, title, nb_users_list, stat_list): + figure = pyplot.figure(figsize=(11.69, 8.29), frameon=False) + figure.subplots_adjust(bottom=0.1, right=0.98, left=0.07, top=0.95) + pyplot.title(title) + pyplot.grid(True, linewidth=1.5) + + axes = figure.add_subplot(111) + + min_array = numpy.array([stat.minimum for stat in stat_list]) + mean_array = numpy.array([stat.mean for stat in stat_list]) + max_array = numpy.array([stat.maximum for stat in stat_list]) + + yerr_list = [stat.standard_deviation for stat in stat_list] + yerr_lower = numpy.minimum(mean_array - min_array, yerr_list) + yerr_upper = numpy.minimum(max_array - mean_array, yerr_list) + + axes.plot(nb_users_list, min_array, 'yo-', label='Minimum') + + axes.errorbar(nb_users_list, + mean_array, + yerr=[yerr_lower, yerr_upper], + color='r', + ecolor='b', + label='Mean', + elinewidth=2, + fmt='D-', + capsize=10.0) + + axes.plot(nb_users_list, max_array, 'gs-', label='Maximum') + + axes.yaxis.set_major_locator(ticker.MultipleLocator(0.5)) + axes.yaxis.set_minor_locator(ticker.MultipleLocator(0.25)) + axes.yaxis.grid(True, 'minor') + + axes.xaxis.set_major_locator(ticker.FixedLocator(nb_users_list)) + + axes.set_xticks(nb_users_list) + axes.legend(loc=0) + axes.set_xlabel('Concurrent users') + axes.set_ylabel('Seconds') + + pyplot.xlim(xmin=nb_users_list[0]) + pdf.savefig() + pylab.close() + +from matplotlib.backends.backend_pdf import PdfPages + +import glob +import os +import re + +user_re = re.compile('-(\d+)users-') + +def generateReport(): + argument_namespace = parseArguments() + + filename_iter = glob.iglob("%s-*repeat*-*users*-*process*.csv" % os.path.join( + argument_namespace.report_directory, + argument_namespace.filename_prefix)) + + per_nb_users_report_dict = {} + for filename in filename_iter: + report_dict = per_nb_users_report_dict.setdefault( + int(user_re.search(filename).group(1)), {'filename': []}) + + report_dict['filename'].append(filename) + + pdf = PdfPages(argument_namespace.output_filename) + + for nb_users, report_dict in per_nb_users_report_dict.items(): + stat_list = computeStatisticFromFilenameList( + argument_namespace, report_dict['filename']) + + title = "Ran suites with %d users" % len(report_dict['filename']) + for slice_start_idx in range(0, len(stat_list), 12): + if slice_start_idx != 0: + title += ' (Ctd.)' + + drawBarDiagram(pdf, title, stat_list[slice_start_idx:slice_start_idx + 12]) + + report_dict['stats'] = stat_list + + if len(per_nb_users_report_dict) != 1: + for i in range(len(report_dict['stats'])): + stat_list = [] + nb_users_list = per_nb_users_report_dict.keys() + for report_dict in per_nb_users_report_dict.values(): + stat_list.append(report_dict['stats'][i]) + + drawConcurrentUsersPlot( + pdf, + "%s from %d to %d users (step: %d)" % (stat_list[0].full_label, + nb_users_list[0], + nb_users_list[-1], + nb_users_list[1] - nb_users_list[0]), + nb_users_list, + stat_list) + + pdf.close() + +if __name__ == '__main__': + generateReport() diff --git a/erp5/util/benchmark/runBenchmark.py b/erp5/util/benchmark/runBenchmark.py new file mode 100755 index 0000000000..cb4e5b0ce0 --- /dev/null +++ b/erp5/util/benchmark/runBenchmark.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python + +############################################################################## +# +# Copyright (c) 2011 Nexedi SA and Contributors. All Rights Reserved. +# Arnaud Fontaine <arnaud.fontaine@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +import argparse +import os + +from benchmark import ArgumentType + +def parseArguments(): + parser = argparse.ArgumentParser(description='Run ERP5 benchmarking suites.') + + # Optional arguments + parser.add_argument('--filename-prefix', + default='result', + metavar='PREFIX', + help='Filename prefix for results and logs files ' + '(default: result)') + + parser.add_argument('--report-directory', + type=ArgumentType.directoryType, + default=os.getcwd(), + metavar='DIRECTORY', + help='Directory where the results and logs will be stored ' + '(default: current directory)') + + parser.add_argument('--max-global-average', + type=float, + default=0, + metavar='N', + help='Stop when any suite operation is over this value ' + '(default: disable)') + + parser.add_argument('--users-file', + dest='user_info_filename', + default='userInfo', + metavar='MODULE', + help="Import users from ``user_tuple'' in MODULE") + + parser.add_argument('--users-range-increment', + type=ArgumentType.strictlyPositiveIntType, + default=1, + metavar='N', + help='Number of users being added after each repetition ' + '(default: 1)') + + parser.add_argument('--enable-debug', + dest='is_debug', + action='store_true', + default=False, + help='Enable debug messages') + + parser.add_argument('--enable-legacy-listbox', + dest='is_legacy_listbox', + action='store_true', + default=False, + help='Enable legacy listbox for Browser') + + parser.add_argument('--repeat', + type=ArgumentType.strictlyPositiveIntType, + default=-1, + metavar='N', + help='Repeat the benchmark suite N times ' + '(default: infinite)') + + # Mandatory arguments + parser.add_argument('url', + type=ArgumentType.ERP5UrlType, + metavar='URL', + help='ERP5 base URL') + + parser.add_argument('users', + type=ArgumentType.strictlyPositiveIntOrRangeType, + metavar='NB_USERS|MIN_NB_USERS,MAX_NB_USERS', + help='Number of users (fixed or a range)') + + parser.add_argument('benchmark_suite_list', + nargs='+', + metavar='BENCHMARK_SUITES', + help='Benchmark suite modules') + + namespace = parser.parse_args() + + namespace.user_tuple = ArgumentType.objectFromModule(namespace.user_info_filename, + object_name='user_tuple') + + object_benchmark_suite_list = [] + for benchmark_suite in namespace.benchmark_suite_list: + object_benchmark_suite_list.append(ArgumentType.objectFromModule(benchmark_suite, + callable_object=True)) + + namespace.benchmark_suite_list = object_benchmark_suite_list + + max_nb_users = isinstance(namespace.users, tuple) and namespace.users[1] or \ + namespace.users + + if max_nb_users > len(namespace.user_tuple): + raise argparse.ArgumentTypeError("Not enough users in the given file") + + return namespace + +import sys +import multiprocessing + +from benchmark import BenchmarkProcess + +def runConstantBenchmark(argument_namespace, nb_users): + process_list = [] + + exit_msg_queue = multiprocessing.Queue(nb_users) + + for user_index in range(nb_users): + process = BenchmarkProcess(exit_msg_queue, nb_users, user_index, argument_namespace) + process_list.append(process) + + for process in process_list: + process.start() + + error_message_set = set() + i = 0 + while i != len(process_list): + try: + msg = exit_msg_queue.get() + except KeyboardInterrupt: + if argument_namespace.repeat != -1: + print >>sys.stderr, "Stopping gracefully" + for process in process_list: + process.terminate() + + i = 0 + continue + + if msg is not None: + error_message_set.add(msg) + for process in process_list: + process.terminate() + + break + + i += 1 + + if error_message_set: + for error_message in error_message_set: + print >>sys.stderr, "ERROR: %s" % error_message + + sys.exit(1) + +def runBenchmark(): + argument_namespace = parseArguments() + + if isinstance(argument_namespace.users, tuple): + nb_users, max_users = argument_namespace.users + while True: + runConstantBenchmark(argument_namespace, nb_users) + + if nb_users == max_users: + break + + nb_users = min(nb_users + argument_namespace.users_range_increment, + max_users) + + else: + runConstantBenchmark(argument_namespace, argument_namespace.users) + +if __name__ == '__main__': + runBenchmark() -- 2.30.9