| #!/usr/bin/env python |
| |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| """Usage: benchmark.py run|list|compare|show|chart <selection> ... |
| |
| SELECTING TIMINGS -- B@R,LxS |
| |
| In the subcommands below, a timings selection consists of a string with up to |
| four elements: |
| <branch>@<revision>,<levels>x<spread> |
| abbreviated as: |
| B@R,LxS |
| |
| <branch> is a label of an svn branch, e.g. "1.7.x". |
| <revision> is the last-changed-revision of above branch. |
| <levels> is the number of directory levels created in the benchmark. |
| <spread> is the number of child trees spreading off each dir level. |
| |
| <branch_name> and <revision> are simply used for labeling. Upon the actual |
| test runs, you should enter labels matching the selected --svn-bin-dir. |
| Later, you can select runs individually by using these labels. |
| |
| For <revision>, you can provide special keywords: |
| - 'each' has the same effect as entering each available revision number that |
| is on record in the db in a separate timings selection. |
| - 'last' is the same as 'each', but shows only the last 10 revisions. 'last' |
| can be combined with a number, e.g. 'last12'. |
| |
| For all subcommands except 'run', you can omit some or all of the elements of |
| a timings selection to combine all available timings sets. Try that out with |
| the 'list' subcommand. |
| |
| Examples: |
| benchmark.py run 1.7.x@12345,5x5 |
| benchmark.py show trunk@12345 |
| benchmark.py compare 1.7.0,1x100 trunk@each,1x100 |
| benchmark.py chart compare 1.7.0,5x5 trunk@last12,5x5 |
| |
| |
| RUN BENCHMARKS |
| |
| benchmark.py run B@R,LxS [N] [options] |
| |
| Test data is added to an sqlite database created automatically, by default |
| 'benchmark.db' in the current working directory. To specify a different path, |
| use option -f <path_to_db>. |
| |
| If <N> is provided, the run is repeated N times. |
| |
| <levels> and <spread> control the way the tested working copy is structured: |
| <levels>: number of directory levels to create. |
| <spread>: number of files and subdirectories created in each dir. |
| |
| |
| LIST WHAT IS ON RECORD |
| |
| benchmark.py list [B@R,LxS] |
| |
| Find entries in the database for the given constraints. Any arguments can |
| be omitted. (To select only a rev, start with a '@', like '@123'; to select |
| only spread, start with an 'x', like "x100".) |
| |
| Call without arguments to get a listing of all available constraints. |
| |
| |
| COMPARE TIMINGS |
| |
| benchmark.py compare B@R,LxS B@R,LxS [B@R,LxS [...]] |
| |
| Compare any number of timings sets to the first provided set (in text mode). |
| For example: |
| benchmark.py compare 1.7.0 trunk@1349903 |
| Compare the total timings of all combined '1.7.0' branch runs to |
| all combined runs of 'trunk'-at-revision-1349903. |
| benchmark.py compare 1.7.0,5x5 trunk@1349903,5x5 |
| Same as above, but only compare the working copy types with 5 levels |
| and a spread of 5. |
| |
| Use the -c option to limit comparison to specific command names. |
| |
| |
| SHOW TIMINGS |
| |
| benchmark.py show B@R,LxS [B@R,LxS [...]] |
| |
| Print out a summary of the timings selected from the given constraints. |
| |
| |
| GENERATE CHARTS |
| |
| benchmark.py chart compare B@R,LxS B@R,LxS [ B@R,LxS ... ] |
| |
| Produce a bar chart that compares any number of sets of timings. Like with |
| the plain 'compare' command, the first set is taken as a reference point for |
| 100% and +-0 seconds. Each following dataset produces a set of labeled bar |
| charts, grouped by svn command names. At least two timings sets must be |
| provided. |
| |
| Use the -c option to limit comparison to specific command names. |
| |
| |
| EXAMPLES |
| |
| # Run 3 benchmarks on svn 1.7.0 with 5 dir levels and 5 files and subdirs for |
| # each level (spread). Timings are saved in ./benchmark.db. |
| # Provide label '1.7.0' and its Last-Changed-Rev for later reference. |
| ./benchmark.py run --svn-bin-dir ~/svn-prefix/1.7.0/bin 1.7.0@1181106,5x5 3 |
| |
| # Record 3 benchmark runs on trunk, again naming its Last-Changed-Rev. |
| # (You may also set your $PATH instead of using --svn-bin-dir.) |
| ./benchmark.py run --svn-bin-dir ~/svn-prefix/trunk/bin trunk@1352725,5x5 3 |
| |
| # Work with the results of above two runs |
| ./benchmark.py list |
| ./benchmark.py compare 1.7.0 trunk |
| ./benchmark.py show 1.7.0 trunk |
| ./benchmark.py chart compare 1.7.0 trunk |
| ./benchmark.py chart compare 1.7.0 trunk -c "update,commit,TOTAL RUN" |
| |
| # Rebuild r1352598, run it and chart improvements since 1.7.0. |
| svn up -r1352598 ~/src/trunk |
| make -C ~/src/trunk dist-clean install |
| export PATH="$HOME/svn-prefix/trunk/bin:$PATH" |
| which svn |
| ./benchmark.py run trunk@1352598,5x5 3 |
| ./benchmark.py chart compare 1.7.0 trunk@1352598 trunk@1352725 -o chart.svg |
| |
| |
| GLOBAL OPTIONS""" |
| |
| import os |
| import time |
| import datetime |
| import sqlite3 |
| import optparse |
| import tempfile |
| import subprocess |
| import random |
| import shutil |
| import stat |
| import string |
| from copy import copy |
| |
| IGNORE_COMMANDS = ('--version', ) |
| TOTAL_RUN = 'TOTAL RUN' |
| |
| j = os.path.join |
| |
| def bail(msg=None): |
| if msg: |
| print msg |
| exit(1) |
| |
| def time_str(): |
| return time.strftime('%Y-%m-%d %H:%M:%S'); |
| |
| def timedelta_to_seconds(td): |
| return ( float(td.seconds) |
| + float(td.microseconds) / (10**6) |
| + td.days * 24 * 60 * 60 ) |
| |
| def run_cmd(cmd, stdin=None, shell=False, verbose=False): |
| if options.verbose: |
| if shell: |
| printable_cmd = cmd |
| else: |
| printable_cmd = ' '.join(cmd) |
| print 'CMD:', printable_cmd |
| |
| if stdin: |
| stdin_arg = subprocess.PIPE |
| else: |
| stdin_arg = None |
| |
| p = subprocess.Popen(cmd, |
| stdin=stdin_arg, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| shell=shell) |
| stdout,stderr = p.communicate(input=stdin) |
| |
| if verbose: |
| if (stdout): |
| print "STDOUT: [[[\n%s]]]" % ''.join(stdout) |
| if (stderr): |
| print "STDERR: [[[\n%s]]]" % ''.join(stderr) |
| |
| return stdout, stderr |
| |
| |
| _next_unique_basename_count = 0 |
| |
| def next_unique_basename(prefix): |
| global _next_unique_basename_count |
| _next_unique_basename_count += 1 |
| return '_'.join((prefix, str(_next_unique_basename_count))) |
| |
| |
| si_units = [ |
| (1000 ** 5, 'P'), |
| (1000 ** 4, 'T'), |
| (1000 ** 3, 'G'), |
| (1000 ** 2, 'M'), |
| (1000 ** 1, 'K'), |
| (1000 ** 0, ''), |
| ] |
| def n_label(n): |
| """(stolen from hurry.filesize)""" |
| for factor, suffix in si_units: |
| if n >= factor: |
| break |
| amount = int(n/factor) |
| if isinstance(suffix, tuple): |
| singular, multiple = suffix |
| if amount == 1: |
| suffix = singular |
| else: |
| suffix = multiple |
| return str(amount) + suffix |
| |
| |
| def split_arg_once(l_r, sep): |
| if not l_r: |
| return (None, None) |
| if sep in l_r: |
| l, r = l_r.split(sep) |
| else: |
| l = l_r |
| r = None |
| if not l: |
| l = None |
| if not r: |
| r = None |
| return (l, r) |
| |
| RUN_KIND_SEPARATORS=('@', ',', 'x') |
| |
| class RunKind: |
| def __init__(self, b_r_l_s): |
| b_r, l_s = split_arg_once(b_r_l_s, RUN_KIND_SEPARATORS[1]) |
| self.branch, self.revision = split_arg_once(b_r, RUN_KIND_SEPARATORS[0]) |
| self.levels, self.spread = split_arg_once(l_s, RUN_KIND_SEPARATORS[2]) |
| if self.levels: self.levels = int(self.levels) |
| if self.spread: self.spread = int(self.spread) |
| |
| def label(self): |
| label_parts = [] |
| if self.branch: |
| label_parts.append(self.branch) |
| if self.revision: |
| label_parts.append(RUN_KIND_SEPARATORS[0]) |
| label_parts.append(self.revision) |
| if self.levels or self.spread: |
| label_parts.append(RUN_KIND_SEPARATORS[1]) |
| if self.levels: |
| label_parts.append(str(self.levels)) |
| if self.spread: |
| label_parts.append(RUN_KIND_SEPARATORS[2]) |
| label_parts.append(str(self.spread)) |
| return ''.join(label_parts) |
| |
| def args(self): |
| return (self.branch, self.revision, self.levels, self.spread) |
| |
| |
| def parse_timings_selections(db, *args): |
| run_kinds = [] |
| |
| for arg in args: |
| run_kind = RunKind(arg) |
| |
| if run_kind.revision == 'each': |
| run_kind.revision = None |
| query = TimingQuery(db, run_kind) |
| for revision in query.get_sorted_revisions(): |
| revision_run_kind = copy(run_kind) |
| revision_run_kind.revision = revision |
| run_kinds.append(revision_run_kind) |
| elif run_kind.revision and run_kind.revision.startswith('last'): |
| Nstr = run_kind.revision[4:] |
| if not Nstr: |
| N = 10 |
| else: |
| N = int(Nstr) |
| run_kind.revision = None |
| query = TimingQuery(db, run_kind) |
| for revision in query.get_sorted_revisions()[-N:]: |
| revision_run_kind = copy(run_kind) |
| revision_run_kind.revision = revision |
| run_kinds.append(revision_run_kind) |
| else: |
| run_kinds.append(run_kind) |
| |
| return run_kinds |
| |
| def parse_one_timing_selection(db, *args): |
| run_kinds = parse_timings_selections(db, *args) |
| if len(run_kinds) != 1: |
| bail("I need exactly one timings identifier, not '%s'" |
| % (' '.join(*args))) |
| return run_kinds[0] |
| |
| |
| |
| |
| PATHNAME_VALID_CHARS = "-_.,@%s%s" % (string.ascii_letters, string.digits) |
| def filesystem_safe_string(s): |
| return ''.join(c for c in s if c in PATHNAME_VALID_CHARS) |
| |
| def do_div(ref, val): |
| if ref: |
| return float(val) / float(ref) |
| else: |
| return 0.0 |
| |
| def do_diff(ref, val): |
| return float(val) - float(ref) |
| |
| |
| # ------------------------- database ------------------------- |
| |
| class TimingsDb: |
| def __init__(self, db_path): |
| self.db_path = db_path; |
| self.conn = sqlite3.connect(db_path) |
| self.ensure_tables_created() |
| |
| def ensure_tables_created(self): |
| c = self.conn.cursor() |
| |
| c.execute("""SELECT name FROM sqlite_master WHERE type='table' AND |
| name='batch'""") |
| if c.fetchone(): |
| # exists |
| return |
| |
| print 'Creating database tables.' |
| c.executescript(''' |
| CREATE TABLE batch ( |
| batch_id INTEGER PRIMARY KEY AUTOINCREMENT, |
| started TEXT, |
| ended TEXT |
| ); |
| |
| CREATE TABLE run_kind ( |
| run_kind_id INTEGER PRIMARY KEY AUTOINCREMENT, |
| branch TEXT NOT NULL, |
| revision TEXT NOT NULL, |
| wc_levels INTEGER, |
| wc_spread INTEGER, |
| UNIQUE(branch, revision, wc_levels, wc_spread) |
| ); |
| |
| CREATE TABLE run ( |
| run_id INTEGER PRIMARY KEY AUTOINCREMENT, |
| batch_id INTEGER NOT NULL REFERENCES batch(batch_id), |
| run_kind_id INTEGER NOT NULL REFERENCES run_kind(run_kind_id), |
| started TEXT, |
| ended TEXT, |
| aborted INTEGER |
| ); |
| |
| CREATE TABLE timings ( |
| run_id INTEGER NOT NULL REFERENCES run(run_id), |
| command TEXT NOT NULL, |
| sequence INTEGER, |
| timing REAL |
| );''' |
| ) |
| self.conn.commit() |
| c.close(); |
| |
| |
| class Batch: |
| def __init__(self, db): |
| self.db = db |
| self.started = time_str() |
| c = db.conn.cursor() |
| c.execute("INSERT INTO batch (started) values (?)", (self.started,)) |
| db.conn.commit() |
| self.id = c.lastrowid |
| c.close() |
| |
| def done(self): |
| conn = self.db.conn |
| c = conn.cursor() |
| c.execute(""" |
| UPDATE batch |
| SET ended = ? |
| WHERE batch_id = ?""", |
| (time_str(), self.id)) |
| conn.commit() |
| c.close() |
| |
| class Run: |
| def __init__(self, batch, run_kind): |
| self.batch = batch |
| conn = self.batch.db.conn |
| c = conn.cursor() |
| |
| c.execute(""" |
| SELECT run_kind_id FROM run_kind |
| WHERE branch = ? |
| AND revision = ? |
| AND wc_levels = ? |
| AND wc_spread = ?""", |
| run_kind.args()) |
| kind_ids = c.fetchone() |
| if kind_ids: |
| kind_id = kind_ids[0] |
| else: |
| c.execute(""" |
| INSERT INTO run_kind (branch, revision, wc_levels, wc_spread) |
| VALUES (?, ?, ?, ?)""", |
| run_kind.args()) |
| conn.commit() |
| kind_id = c.lastrowid |
| |
| self.started = time_str() |
| |
| c.execute(""" |
| INSERT INTO run |
| (batch_id, run_kind_id, started) |
| VALUES |
| (?, ?, ?)""", |
| (self.batch.id, kind_id, self.started)) |
| conn.commit() |
| self.id = c.lastrowid |
| c.close(); |
| self.tic_at = None |
| self.current_command = None |
| self.timings = [] |
| |
| def tic(self, command): |
| if command in IGNORE_COMMANDS: |
| return |
| self.toc() |
| self.current_command = command |
| self.tic_at = datetime.datetime.now() |
| |
| def toc(self): |
| if self.current_command and self.tic_at: |
| toc_at = datetime.datetime.now() |
| self.remember_timing(self.current_command, |
| timedelta_to_seconds(toc_at - self.tic_at)) |
| self.current_command = None |
| self.tic_at = None |
| |
| def remember_timing(self, command, seconds): |
| self.timings.append((command, seconds)) |
| |
| def submit_timings(self): |
| conn = self.batch.db.conn |
| c = conn.cursor() |
| print 'submitting...' |
| |
| c.executemany(""" |
| INSERT INTO timings |
| (run_id, command, sequence, timing) |
| VALUES |
| (?, ?, ?, ?)""", |
| [(self.id, t[0], (i + 1), t[1]) for i,t in enumerate(self.timings)]) |
| |
| conn.commit() |
| c.close() |
| |
| def done(self, aborted=False): |
| conn = self.batch.db.conn |
| c = conn.cursor() |
| c.execute(""" |
| UPDATE run |
| SET ended = ?, aborted = ? |
| WHERE run_id = ?""", |
| (time_str(), aborted, self.id)) |
| conn.commit() |
| c.close() |
| |
| |
| class TimingQuery: |
| def __init__(self, db, run_kind): |
| self.cursor = db.conn.cursor() |
| self.constraints = [] |
| self.values = [] |
| self.timings = None |
| self.FROM_WHERE = """ |
| FROM batch AS b, |
| timings AS t, |
| run AS r, |
| run_kind as k |
| WHERE |
| t.run_id = r.run_id |
| AND k.run_kind_id = r.run_kind_id |
| AND b.batch_id = r.batch_id |
| AND r.aborted = 0 |
| """ |
| self.append_constraint('k.branch', run_kind.branch) |
| self.each_revision = False |
| if run_kind.revision == 'each': |
| self.each_revision = True |
| else: |
| self.append_constraint('k.revision', run_kind.revision) |
| self.append_constraint('k.wc_levels', run_kind.levels) |
| self.append_constraint('k.wc_spread', run_kind.spread) |
| self.label = run_kind.label() |
| |
| def append_constraint(self, column_name, val): |
| if val: |
| self.constraints.append('AND %s = ?' % column_name) |
| self.values.append(val) |
| |
| def remove_last_constraint(self): |
| del self.constraints[-1] |
| del self.values[-1] |
| |
| def get_sorted_X(self, x, n=1): |
| query = ['SELECT DISTINCT %s' % x, |
| self.FROM_WHERE ] |
| query.extend(self.constraints) |
| query.append('ORDER BY %s' % x) |
| c = db.conn.cursor() |
| try: |
| c.execute(' '.join(query), self.values) |
| if n == 1: |
| return [tpl[0] for tpl in c.fetchall()] |
| else: |
| return c.fetchall() |
| finally: |
| c.close() |
| |
| def get_sorted_command_names(self): |
| return self.get_sorted_X('t.command') |
| |
| def get_sorted_branches(self): |
| return self.get_sorted_X('k.branch') |
| |
| def get_sorted_revisions(self): |
| return self.get_sorted_X('k.revision') |
| |
| def get_sorted_levels_spread(self): |
| return self.get_sorted_X('k.wc_levels,k.wc_spread', n = 2) |
| |
| def count_runs_batches(self): |
| query = ["""SELECT |
| count(DISTINCT r.run_id), |
| count(DISTINCT b.batch_id)""", |
| self.FROM_WHERE ] |
| query.extend(self.constraints) |
| c = db.conn.cursor() |
| try: |
| #print ' '.join(query) |
| c.execute(' '.join(query), self.values) |
| return c.fetchone() |
| finally: |
| c.close() |
| |
| def get_command_timings(self, command): |
| query = ["""SELECT |
| count(t.timing), |
| min(t.timing), |
| max(t.timing), |
| avg(t.timing)""", |
| self.FROM_WHERE ] |
| self.append_constraint('t.command', command) |
| try: |
| query.extend(self.constraints) |
| c = db.conn.cursor() |
| try: |
| c.execute(' '.join(query), self.values) |
| return c.fetchone() |
| finally: |
| c.close() |
| finally: |
| self.remove_last_constraint() |
| |
| def get_timings(self): |
| if self.timings: |
| return self.timings |
| self.timings = {} |
| for command_name in self.get_sorted_command_names(): |
| self.timings[command_name] = self.get_command_timings(command_name) |
| return self.timings |
| |
| |
| # ------------------------------------------------------------ run tests |
| |
| |
| def perform_run(batch, run_kind, |
| svn_bin, svnadmin_bin, verbose): |
| |
| run = Run(batch, run_kind) |
| |
| def create_tree(in_dir, _levels, _spread): |
| try: |
| os.mkdir(in_dir) |
| except: |
| pass |
| |
| for i in range(_spread): |
| # files |
| fn = j(in_dir, next_unique_basename('file')) |
| f = open(fn, 'w') |
| f.write('This is %s\n' % fn) |
| f.close() |
| |
| # dirs |
| if (_levels > 1): |
| dn = j(in_dir, next_unique_basename('dir')) |
| create_tree(dn, _levels - 1, _spread) |
| |
| def svn(*args): |
| name = args[0] |
| |
| cmd = [ svn_bin ] |
| cmd.extend( list(args) ) |
| if verbose: |
| print 'svn cmd:', ' '.join(cmd) |
| |
| stdin = None |
| if stdin: |
| stdin_arg = subprocess.PIPE |
| else: |
| stdin_arg = None |
| |
| run.tic(name) |
| try: |
| p = subprocess.Popen(cmd, |
| stdin=stdin_arg, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| shell=False) |
| stdout,stderr = p.communicate(input=stdin) |
| except OSError: |
| stdout = stderr = None |
| finally: |
| run.toc() |
| |
| if verbose: |
| if (stdout): |
| print "STDOUT: [[[\n%s]]]" % ''.join(stdout) |
| if (stderr): |
| print "STDERR: [[[\n%s]]]" % ''.join(stderr) |
| |
| return stdout,stderr |
| |
| |
| def add(*args): |
| return svn('add', *args) |
| |
| def ci(*args): |
| return svn('commit', '-mm', *args) |
| |
| def up(*args): |
| return svn('update', *args) |
| |
| def st(*args): |
| return svn('status', *args) |
| |
| def info(*args): |
| return svn('info', *args) |
| |
| _chars = [chr(x) for x in range(ord('a'), ord('z') +1)] |
| |
| def randstr(len=8): |
| return ''.join( [random.choice(_chars) for i in range(len)] ) |
| |
| def _copy(path): |
| dest = next_unique_basename(path + '_copied') |
| svn('copy', path, dest) |
| |
| def _move(path): |
| dest = path + '_moved' |
| svn('move', path, dest) |
| |
| def _propmod(path): |
| so, se = svn('proplist', path) |
| propnames = [line.strip() for line in so.strip().split('\n')[1:]] |
| |
| # modify? |
| if len(propnames): |
| svn('ps', propnames[len(propnames) / 2], randstr(), path) |
| |
| # del? |
| if len(propnames) > 1: |
| svn('propdel', propnames[len(propnames) / 2], path) |
| |
| def _propadd(path): |
| # set a new one. |
| svn('propset', randstr(), randstr(), path) |
| |
| def _mod(path): |
| if os.path.isdir(path): |
| _propmod(path) |
| return |
| |
| f = open(path, 'a') |
| f.write('\n%s\n' % randstr()) |
| f.close() |
| |
| def _add(path): |
| if os.path.isfile(path): |
| return _mod(path) |
| |
| if random.choice((True, False)): |
| # create a dir |
| svn('mkdir', j(path, next_unique_basename('new_dir'))) |
| else: |
| # create a file |
| new_path = j(path, next_unique_basename('new_file')) |
| f = open(new_path, 'w') |
| f.write(randstr()) |
| f.close() |
| svn('add', new_path) |
| |
| def _del(path): |
| svn('delete', path) |
| |
| _mod_funcs = (_mod, _add, _propmod, _propadd, )#_copy,) # _move, _del) |
| |
| def modify_tree(in_dir, fraction): |
| child_names = os.listdir(in_dir) |
| for child_name in child_names: |
| if child_name[0] == '.': |
| continue |
| if random.random() < fraction: |
| path = j(in_dir, child_name) |
| random.choice(_mod_funcs)(path) |
| |
| for child_name in child_names: |
| if child_name[0] == '.': continue |
| path = j(in_dir, child_name) |
| if os.path.isdir(path): |
| modify_tree(path, fraction) |
| |
| def propadd_tree(in_dir, fraction): |
| for child_name in os.listdir(in_dir): |
| if child_name[0] == '.': continue |
| path = j(in_dir, child_name) |
| if random.random() < fraction: |
| _propadd(path) |
| if os.path.isdir(path): |
| propadd_tree(path, fraction) |
| |
| |
| def rmtree_onerror(func, path, exc_info): |
| """Error handler for ``shutil.rmtree``. |
| |
| If the error is due to an access error (read only file) |
| it attempts to add write permission and then retries. |
| |
| If the error is for another reason it re-raises the error. |
| |
| Usage : ``shutil.rmtree(path, onerror=onerror)`` |
| """ |
| if not os.access(path, os.W_OK): |
| # Is the error an access error ? |
| os.chmod(path, stat.S_IWUSR) |
| func(path) |
| else: |
| raise |
| |
| base = tempfile.mkdtemp() |
| |
| # ensure identical modifications for every run |
| random.seed(0) |
| |
| aborted = True |
| |
| try: |
| repos = j(base, 'repos') |
| repos = repos.replace('\\', '/') |
| wc = j(base, 'wc') |
| wc2 = j(base, 'wc2') |
| |
| if repos.startswith('/'): |
| file_url = 'file://%s' % repos |
| else: |
| file_url = 'file:///%s' % repos |
| |
| print '\nRunning svn benchmark in', base |
| print 'dir levels: %s; new files and dirs per leaf: %s' %( |
| run_kind.levels, run_kind.spread) |
| |
| started = datetime.datetime.now() |
| |
| try: |
| run_cmd([svnadmin_bin, 'create', repos]) |
| svn('checkout', file_url, wc) |
| |
| trunk = j(wc, 'trunk') |
| create_tree(trunk, run_kind.levels, run_kind.spread) |
| add(trunk) |
| st(wc) |
| ci(wc) |
| up(wc) |
| propadd_tree(trunk, 0.05) |
| ci(wc) |
| up(wc) |
| st(wc) |
| info('-R', wc) |
| |
| trunk_url = file_url + '/trunk' |
| branch_url = file_url + '/branch' |
| |
| svn('copy', '-mm', trunk_url, branch_url) |
| st(wc) |
| |
| up(wc) |
| st(wc) |
| info('-R', wc) |
| |
| svn('checkout', trunk_url, wc2) |
| st(wc2) |
| modify_tree(wc2, 0.5) |
| st(wc2) |
| ci(wc2) |
| up(wc2) |
| up(wc) |
| |
| svn('switch', branch_url, wc2) |
| modify_tree(wc2, 0.5) |
| st(wc2) |
| info('-R', wc2) |
| ci(wc2) |
| up(wc2) |
| up(wc) |
| |
| modify_tree(trunk, 0.5) |
| st(wc) |
| ci(wc) |
| up(wc2) |
| up(wc) |
| |
| svn('merge', '--accept=postpone', trunk_url, wc2) |
| st(wc2) |
| info('-R', wc2) |
| svn('resolve', '--accept=mine-conflict', wc2) |
| st(wc2) |
| svn('resolved', '-R', wc2) |
| st(wc2) |
| info('-R', wc2) |
| ci(wc2) |
| up(wc2) |
| up(wc) |
| |
| svn('merge', '--accept=postpone', '--reintegrate', branch_url, trunk) |
| st(wc) |
| svn('resolve', '--accept=mine-conflict', wc) |
| st(wc) |
| svn('resolved', '-R', wc) |
| st(wc) |
| ci(wc) |
| up(wc2) |
| up(wc) |
| |
| svn('delete', j(wc, 'branch')) |
| ci(wc) |
| up(wc) |
| |
| aborted = False |
| |
| finally: |
| stopped = datetime.datetime.now() |
| print '\nDone with svn benchmark in', (stopped - started) |
| |
| run.remember_timing(TOTAL_RUN, |
| timedelta_to_seconds(stopped - started)) |
| finally: |
| run.done(aborted) |
| run.submit_timings() |
| shutil.rmtree(base, onerror=rmtree_onerror) |
| |
| return aborted |
| |
| |
| # --------------------------------------------------------------------- |
| |
| |
| def cmdline_run(db, options, run_kind_str, N=1): |
| run_kind = parse_one_timing_selection(db, run_kind_str) |
| |
| N = int(N) |
| |
| print 'Hi, going to run a Subversion benchmark series of %d runs...' % N |
| print 'Label is %s' % run_kind.label() |
| |
| # can we run the svn binaries? |
| svn_bin = j(options.svn_bin_dir, 'svn') |
| svnadmin_bin = j(options.svn_bin_dir, 'svnadmin') |
| |
| for b in (svn_bin, svnadmin_bin): |
| so,se = run_cmd([b, '--version']) |
| if not so: |
| bail("Can't run %s" % b) |
| |
| print ', '.join([s.strip() for s in so.split('\n')[:2]]) |
| |
| batch = Batch(db) |
| |
| for i in range(N): |
| print 'Run %d of %d' % (i + 1, N) |
| perform_run(batch, run_kind, |
| svn_bin, svnadmin_bin, options.verbose) |
| |
| batch.done() |
| |
| |
| def cmdline_list(db, options, *args): |
| run_kinds = parse_timings_selections(db, *args) |
| |
| for run_kind in run_kinds: |
| |
| constraints = [] |
| def add_if_not_none(name, val): |
| if val: |
| constraints.append(' %s = %s' % (name, val)) |
| add_if_not_none('branch', run_kind.branch) |
| add_if_not_none('revision', run_kind.revision) |
| add_if_not_none('levels', run_kind.levels) |
| add_if_not_none('spread', run_kind.spread) |
| if constraints: |
| print 'For\n', '\n'.join(constraints) |
| print 'I found:' |
| |
| d = TimingQuery(db, run_kind) |
| |
| cmd_names = d.get_sorted_command_names() |
| if cmd_names: |
| print '\n%d command names:\n ' % len(cmd_names), '\n '.join(cmd_names) |
| |
| branches = d.get_sorted_branches() |
| if branches and (len(branches) > 1 or branches[0] != run_kind.branch): |
| print '\n%d branches:\n ' % len(branches), '\n '.join(branches) |
| |
| revisions = d.get_sorted_revisions() |
| if revisions and (len(revisions) > 1 or revisions[0] != run_kind.revision): |
| print '\n%d revisions:\n ' % len(revisions), '\n '.join(revisions) |
| |
| levels_spread = d.get_sorted_levels_spread() |
| if levels_spread and ( |
| len(levels_spread) > 1 |
| or levels_spread[0] != (run_kind.levels, run_kind.spread)): |
| print '\n%d kinds of levels x spread:\n ' % len(levels_spread), '\n '.join( |
| [ ('%dx%d' % (l, s)) for l,s in levels_spread ]) |
| |
| print "\n%d runs in %d batches.\n" % (d.count_runs_batches()) |
| |
| |
| def cmdline_show(db, options, *run_kind_strings): |
| run_kinds = parse_timings_selections(db, *run_kind_strings) |
| for run_kind in run_kinds: |
| q = TimingQuery(db, run_kind) |
| timings = q.get_timings() |
| |
| s = [] |
| s.append('Timings for %s' % run_kind.label()) |
| s.append(' N min max avg operation (unit is seconds)') |
| |
| for command_name in q.get_sorted_command_names(): |
| if options.command_names and command_name not in options.command_names: |
| continue |
| n, tmin, tmax, tavg = timings[command_name] |
| |
| s.append('%4s %7.2f %7.2f %7.2f %s' % ( |
| n_label(n), |
| tmin, |
| tmax, |
| tavg, |
| command_name)) |
| |
| print '\n'.join(s) |
| |
| |
| def cmdline_compare(db, options, *args): |
| run_kinds = parse_timings_selections(db, *args) |
| if len(run_kinds) < 2: |
| bail("Need at least two sets of timings to compare.") |
| |
| |
| left_kind = run_kinds[0] |
| leftq = TimingQuery(db, left_kind) |
| left = leftq.get_timings() |
| if not left: |
| bail("No timings for %s" % left_kind.label()) |
| |
| for run_kind_idx in range(1, len(run_kinds)): |
| right_kind = run_kinds[run_kind_idx] |
| |
| rightq = TimingQuery(db, right_kind) |
| right = rightq.get_timings() |
| if not right: |
| print "No timings for %s" % right_kind.label() |
| continue |
| |
| label = 'Compare %s to %s' % (right_kind.label(), left_kind.label()) |
| |
| s = [label] |
| |
| verbose = options.verbose |
| if not verbose: |
| s.append(' N avg operation') |
| else: |
| s.append(' N min max avg operation') |
| |
| command_names = [name for name in leftq.get_sorted_command_names() |
| if name in right] |
| if options.command_names: |
| command_names = [name for name in command_names |
| if name in options.command_names] |
| |
| for command_name in command_names: |
| left_N, left_min, left_max, left_avg = left[command_name] |
| right_N, right_min, right_max, right_avg = right[command_name] |
| |
| N_str = '%s/%s' % (n_label(left_N), n_label(right_N)) |
| avg_str = '%7.2f|%+7.3f' % (do_div(left_avg, right_avg), |
| do_diff(left_avg, right_avg)) |
| |
| if not verbose: |
| s.append('%9s %-16s %s' % (N_str, avg_str, command_name)) |
| else: |
| min_str = '%7.2f|%+7.3f' % (do_div(left_min, right_min), |
| do_diff(left_min, right_min)) |
| max_str = '%7.2f|%+7.3f' % (do_div(left_max, right_max), |
| do_diff(left_max, right_max)) |
| |
| s.append('%9s %-16s %-16s %-16s %s' % (N_str, min_str, max_str, avg_str, |
| command_name)) |
| |
| s.extend([ |
| '(legend: "1.23|+0.45" means: slower by factor 1.23 and by 0.45 seconds;', |
| ' factor < 1 and seconds < 0 means \'%s\' is faster.' |
| % right_kind.label(), |
| ' "2/3" means: \'%s\' has 2 timings on record, the other has 3.)' |
| % left_kind.label() |
| ]) |
| |
| |
| print '\n'.join(s) |
| |
| |
| # ------------------------------------------------------- charts |
| |
| def cmdline_chart_compare(db, options, *args): |
| import matplotlib |
| matplotlib.use('Agg') |
| import numpy as np |
| import matplotlib.pylab as plt |
| |
| labels = [] |
| timing_sets = [] |
| command_names = None |
| |
| run_kinds = parse_timings_selections(db, *args) |
| |
| # iterate the timings selections and accumulate data |
| for run_kind in run_kinds: |
| query = TimingQuery(db, run_kind) |
| timings = query.get_timings() |
| if not timings: |
| print "No timings for %s" % run_kind.label() |
| continue |
| labels.append(run_kind.label()) |
| timing_sets.append(timings) |
| |
| # it only makes sense to compare those commands that have timings |
| # in the first selection, because that is the one everything else |
| # is compared to. Remember the first selection's command names. |
| if not command_names: |
| command_names = query.get_sorted_command_names() |
| |
| |
| if len(timing_sets) < 2: |
| bail("Not enough timings") |
| |
| if options.command_names: |
| command_names = [name for name in command_names |
| if name in options.command_names] |
| |
| chart_path = options.chart_path |
| if not chart_path: |
| chart_path = 'compare_' + '_'.join( |
| [ filesystem_safe_string(l) for l in labels ] |
| ) + '.svg' |
| |
| N = len(command_names) |
| M = len(timing_sets) - 1 |
| if M < 2: |
| M = 2 |
| |
| group_positions = np.arange(N) # the y locations for the groups |
| dist = 1. / (1. + M) |
| height = (1. - dist) / M # the height of the bars |
| |
| fig = plt.figure(figsize=(12, 5 + 0.2*N*M)) |
| plot1 = fig.add_subplot(121) |
| plot2 = fig.add_subplot(122) |
| |
| left = timing_sets[0] |
| |
| # Iterate timing sets. Each loop produces one bar for each command name |
| # group. |
| for label_i,label in enumerate(labels[1:],1): |
| right = timing_sets[label_i] |
| if not right: |
| continue |
| |
| for cmd_i, command_name in enumerate(command_names): |
| if command_name not in right: |
| #skip |
| continue |
| |
| left_N, left_min, left_max, left_avg = left[command_name] |
| right_N, right_min, right_max, right_avg = right[command_name] |
| |
| div_avg = 100. * (do_div(left_avg, right_avg) - 1.0) |
| if div_avg <= 0: |
| col = '#55dd55' |
| else: |
| col = '#dd5555' |
| |
| diff_val = do_diff(left_avg, right_avg) |
| |
| ofs = (dist + height) / 2. + height * (label_i - 1) |
| |
| barheight = height * (1.0 - dist) |
| |
| y = float(cmd_i) + ofs |
| |
| plot1.barh((y, ), |
| (div_avg, ), |
| barheight, |
| color=col, edgecolor='white') |
| plot1.text(0., y + height/2., |
| '%s %+5.1f%%' % (label, div_avg), |
| ha='right', va='center', size='small', |
| rotation=0, family='monospace') |
| |
| plot2.barh((y, ), |
| (diff_val, ), |
| barheight, |
| color=col, edgecolor='white') |
| plot2.text(0., y + height/2., |
| '%s %+6.2fs' % (label, diff_val), |
| ha='right', va='center', size='small', |
| rotation=0, family='monospace') |
| |
| |
| for p in (plot1, plot2): |
| xlim = list(p.get_xlim()) |
| if xlim[1] < 10.: |
| xlim[1] = 10. |
| # make sure the zero line is far enough right so that the annotations |
| # fit inside the chart. About half the width should suffice. |
| if xlim[0] > -xlim[1]: |
| xlim[0] = -xlim[1] |
| p.set_xlim(*xlim) |
| p.set_xticks((0,)) |
| p.set_yticks(group_positions + (height / 2.)) |
| p.set_yticklabels(()) |
| p.set_ylim((len(command_names), 0)) |
| p.grid() |
| |
| plot1.set_xticklabels(('+-0%',), rotation=0) |
| plot1.set_title('Average runtime change from %s in %%' % labels[0], |
| size='medium') |
| |
| plot2.set_xticklabels(('+-0s',), rotation=0) |
| plot2.set_title('Average runtime change from %s in seconds' % labels[0], |
| size='medium') |
| |
| margin = 1./(2 + N*M) |
| titlemargin = 0 |
| if options.title: |
| titlemargin = margin * 1.5 |
| |
| fig.subplots_adjust(left=0.005, right=0.995, wspace=0.3, bottom=margin, |
| top=1.0-margin-titlemargin) |
| |
| ystep = (1.0 - 2.*margin - titlemargin) / len(command_names) |
| |
| for idx,command_name in enumerate(command_names): |
| ylabel = '%s\nvs. %.1fs' % ( |
| command_name, |
| left[command_name][3]) |
| |
| ypos=1.0 - margin - titlemargin - ystep/M - ystep * idx |
| plt.figtext(0.5, ypos, |
| command_name, |
| ha='center', va='top', |
| size='medium', weight='bold') |
| plt.figtext(0.5, ypos - ystep/(M+1), |
| '%s\n= %.2fs' % ( |
| labels[0], left[command_name][3]), |
| ha='center', va='top', |
| size='small') |
| |
| if options.title: |
| plt.figtext(0.5, 1. - titlemargin/2, options.title, ha='center', |
| va='center', weight='bold') |
| |
| plt.savefig(chart_path) |
| print 'wrote chart file:', chart_path |
| |
| |
| # ------------------------------------------------------------ main |
| |
| |
| # Custom option formatter, keeping newlines in the description. |
| # adapted from: |
| # http://groups.google.com/group/comp.lang.python/msg/09f28e26af0699b1 |
| import textwrap |
| class IndentedHelpFormatterWithNL(optparse.IndentedHelpFormatter): |
| def format_description(self, description): |
| if not description: return "" |
| desc_width = self.width - self.current_indent |
| indent = " "*self.current_indent |
| bits = description.split('\n') |
| formatted_bits = [ |
| textwrap.fill(bit, |
| desc_width, |
| initial_indent=indent, |
| subsequent_indent=indent) |
| for bit in bits] |
| result = "\n".join(formatted_bits) + "\n" |
| return result |
| |
| if __name__ == '__main__': |
| parser = optparse.OptionParser(formatter=IndentedHelpFormatterWithNL()) |
| # -h is automatically added. |
| ### should probably expand the help for that. and see about -? |
| parser.add_option('-v', '--verbose', action='store_true', dest='verbose', |
| help='Verbose operation') |
| parser.add_option('-b', '--svn-bin-dir', action='store', dest='svn_bin_dir', |
| default='', |
| help='Specify directory to find Subversion binaries in') |
| parser.add_option('-f', '--db-path', action='store', dest='db_path', |
| default='benchmark.db', |
| help='Specify path to SQLite database file') |
| parser.add_option('-o', '--chart-path', action='store', dest='chart_path', |
| help='Supply a path for chart output.') |
| parser.add_option('-c', '--command-names', action='store', |
| dest='command_names', |
| help='Comma separated list of command names to limit to.') |
| parser.add_option('-t', '--title', action='store', |
| dest='title', |
| help='For charts, a title to print in the chart graphics.') |
| |
| parser.set_description(__doc__) |
| parser.set_usage('') |
| |
| |
| options, args = parser.parse_args() |
| |
| def usage(msg=None): |
| parser.print_help() |
| if msg: |
| print |
| print msg |
| bail() |
| |
| # there should be at least one arg left: the sub-command |
| if not args: |
| usage('No command argument supplied.') |
| |
| cmd = args[0] |
| del args[0] |
| |
| db = TimingsDb(options.db_path) |
| |
| if cmd == 'run': |
| if len(args) < 1 or len(args) > 2: |
| usage() |
| cmdline_run(db, options, *args) |
| |
| elif cmd == 'compare': |
| if len(args) < 2: |
| usage() |
| cmdline_compare(db, options, *args) |
| |
| elif cmd == 'list': |
| cmdline_list(db, options, *args) |
| |
| elif cmd == 'show': |
| cmdline_show(db, options, *args) |
| |
| elif cmd == 'chart': |
| if 'compare'.startswith(args[0]): |
| cmdline_chart_compare(db, options, *args[1:]) |
| else: |
| usage() |
| |
| else: |
| usage('Unknown subcommand argument: %s' % cmd) |