67
196
stdout,stderr = p.communicate(input=stdin)
71
200
print "STDOUT: [[[\n%s]]]" % ''.join(stdout)
73
202
print "STDERR: [[[\n%s]]]" % ''.join(stderr)
77
def timedelta_to_seconds(td):
78
return ( float(td.seconds)
79
+ float(td.microseconds) / (10**6)
80
+ td.days * 24 * 60 * 60 )
85
def __init__(self, *ignore_svn_cmds):
87
self.current_name = None
204
return stdout, stderr
207
_next_unique_basename_count = 0
209
def next_unique_basename(prefix):
210
global _next_unique_basename_count
211
_next_unique_basename_count += 1
212
return '_'.join((prefix, str(_next_unique_basename_count)))
224
"""(stolen from hurry.filesize)"""
225
for factor, suffix in si_units:
228
amount = int(n/factor)
229
if isinstance(suffix, tuple):
230
singular, multiple = suffix
235
return str(amount) + suffix
238
def split_arg_once(l_r, sep):
242
l, r = l_r.split(sep)
252
RUN_KIND_SEPARATORS=('@', ',', 'x')
255
def __init__(self, b_r_l_s):
256
b_r, l_s = split_arg_once(b_r_l_s, RUN_KIND_SEPARATORS[1])
257
self.branch, self.revision = split_arg_once(b_r, RUN_KIND_SEPARATORS[0])
258
self.levels, self.spread = split_arg_once(l_s, RUN_KIND_SEPARATORS[2])
259
if self.levels: self.levels = int(self.levels)
260
if self.spread: self.spread = int(self.spread)
265
label_parts.append(self.branch)
267
label_parts.append(RUN_KIND_SEPARATORS[0])
268
label_parts.append(self.revision)
269
if self.levels or self.spread:
270
label_parts.append(RUN_KIND_SEPARATORS[1])
272
label_parts.append(str(self.levels))
274
label_parts.append(RUN_KIND_SEPARATORS[2])
275
label_parts.append(str(self.spread))
276
return ''.join(label_parts)
279
return (self.branch, self.revision, self.levels, self.spread)
282
def parse_timings_selections(db, *args):
286
run_kind = RunKind(arg)
288
if run_kind.revision == 'each':
289
run_kind.revision = None
290
query = TimingQuery(db, run_kind)
291
for revision in query.get_sorted_revisions():
292
revision_run_kind = copy(run_kind)
293
revision_run_kind.revision = revision
294
run_kinds.append(revision_run_kind)
295
elif run_kind.revision and run_kind.revision.startswith('last'):
296
Nstr = run_kind.revision[4:]
301
run_kind.revision = None
302
query = TimingQuery(db, run_kind)
303
for revision in query.get_sorted_revisions()[-N:]:
304
revision_run_kind = copy(run_kind)
305
revision_run_kind.revision = revision
306
run_kinds.append(revision_run_kind)
308
run_kinds.append(run_kind)
312
def parse_one_timing_selection(db, *args):
313
run_kinds = parse_timings_selections(db, *args)
314
if len(run_kinds) != 1:
315
bail("I need exactly one timings identifier, not '%s'"
322
PATHNAME_VALID_CHARS = "-_.,@%s%s" % (string.ascii_letters, string.digits)
323
def filesystem_safe_string(s):
324
return ''.join(c for c in s if c in PATHNAME_VALID_CHARS)
326
def do_div(ref, val):
328
return float(val) / float(ref)
332
def do_diff(ref, val):
333
return float(val) - float(ref)
336
# ------------------------- database -------------------------
339
def __init__(self, db_path):
340
self.db_path = db_path;
341
self.conn = sqlite3.connect(db_path)
342
self.ensure_tables_created()
344
def ensure_tables_created(self):
345
c = self.conn.cursor()
347
c.execute("""SELECT name FROM sqlite_master WHERE type='table' AND
353
print 'Creating database tables.'
356
batch_id INTEGER PRIMARY KEY AUTOINCREMENT,
361
CREATE TABLE run_kind (
362
run_kind_id INTEGER PRIMARY KEY AUTOINCREMENT,
363
branch TEXT NOT NULL,
364
revision TEXT NOT NULL,
367
UNIQUE(branch, revision, wc_levels, wc_spread)
371
run_id INTEGER PRIMARY KEY AUTOINCREMENT,
372
batch_id INTEGER NOT NULL REFERENCES batch(batch_id),
373
run_kind_id INTEGER NOT NULL REFERENCES run_kind(run_kind_id),
379
CREATE TABLE timings (
380
run_id INTEGER NOT NULL REFERENCES run(run_id),
381
command TEXT NOT NULL,
391
def __init__(self, db):
393
self.started = time_str()
395
c.execute("INSERT INTO batch (started) values (?)", (self.started,))
397
self.id = c.lastrowid
406
WHERE batch_id = ?""",
407
(time_str(), self.id))
412
def __init__(self, batch, run_kind):
414
conn = self.batch.db.conn
418
SELECT run_kind_id FROM run_kind
422
AND wc_spread = ?""",
424
kind_ids = c.fetchone()
426
kind_id = kind_ids[0]
429
INSERT INTO run_kind (branch, revision, wc_levels, wc_spread)
430
VALUES (?, ?, ?, ?)""",
433
kind_id = c.lastrowid
435
self.started = time_str()
439
(batch_id, run_kind_id, started)
442
(self.batch.id, kind_id, self.started))
444
self.id = c.lastrowid
88
446
self.tic_at = None
89
self.ignore = ignore_svn_cmds
447
self.current_command = None
93
if name in self.ignore:
450
def tic(self, command):
451
if command in IGNORE_COMMANDS:
96
self.current_name = name
454
self.current_command = command
97
455
self.tic_at = datetime.datetime.now()
100
if self.current_name and self.tic_at:
458
if self.current_command and self.tic_at:
101
459
toc_at = datetime.datetime.now()
102
self.submit_timing(self.current_name,
460
self.remember_timing(self.current_command,
103
461
timedelta_to_seconds(toc_at - self.tic_at))
104
self.current_name = None
462
self.current_command = None
105
463
self.tic_at = None
107
def submit_timing(self, name, seconds):
108
times = self.timings.get(name)
111
self.timings[name] = times
112
times.append(seconds)
114
def min_max_avg(self, name):
115
ttimings = self.timings.get(name)
116
return ( min(ttimings),
118
reduce(lambda x,y: x + y, ttimings) / len(ttimings) )
465
def remember_timing(self, command, seconds):
466
self.timings.append((command, seconds))
468
def submit_timings(self):
469
conn = self.batch.db.conn
471
print 'submitting...'
475
(run_id, command, sequence, timing)
478
[(self.id, t[0], (i + 1), t[1]) for i,t in enumerate(self.timings)])
483
def done(self, aborted=False):
484
conn = self.batch.db.conn
488
SET ended = ?, aborted = ?
490
(time_str(), aborted, self.id))
496
def __init__(self, db, run_kind):
497
self.cursor = db.conn.cursor()
498
self.constraints = []
501
self.FROM_WHERE = """
508
AND k.run_kind_id = r.run_kind_id
509
AND b.batch_id = r.batch_id
512
self.append_constraint('k.branch', run_kind.branch)
513
self.each_revision = False
514
if run_kind.revision == 'each':
515
self.each_revision = True
517
self.append_constraint('k.revision', run_kind.revision)
518
self.append_constraint('k.wc_levels', run_kind.levels)
519
self.append_constraint('k.wc_spread', run_kind.spread)
520
self.label = run_kind.label()
522
def append_constraint(self, column_name, val):
524
self.constraints.append('AND %s = ?' % column_name)
525
self.values.append(val)
527
def remove_last_constraint(self):
528
del self.constraints[-1]
531
def get_sorted_X(self, x, n=1):
532
query = ['SELECT DISTINCT %s' % x,
534
query.extend(self.constraints)
535
query.append('ORDER BY %s' % x)
538
c.execute(' '.join(query), self.values)
540
return [tpl[0] for tpl in c.fetchall()]
546
def get_sorted_command_names(self):
547
return self.get_sorted_X('t.command')
549
def get_sorted_branches(self):
550
return self.get_sorted_X('k.branch')
552
def get_sorted_revisions(self):
553
return self.get_sorted_X('k.revision')
555
def get_sorted_levels_spread(self):
556
return self.get_sorted_X('k.wc_levels,k.wc_spread', n = 2)
558
def count_runs_batches(self):
560
count(DISTINCT r.run_id),
561
count(DISTINCT b.batch_id)""",
563
query.extend(self.constraints)
566
#print ' '.join(query)
567
c.execute(' '.join(query), self.values)
572
def get_command_timings(self, command):
579
self.append_constraint('t.command', command)
581
query.extend(self.constraints)
584
c.execute(' '.join(query), self.values)
589
self.remove_last_constraint()
591
def get_timings(self):
595
for command_name in self.get_sorted_command_names():
596
self.timings[command_name] = self.get_command_timings(command_name)
600
# ------------------------------------------------------------ run tests
603
def perform_run(batch, run_kind,
604
svn_bin, svnadmin_bin, verbose):
606
run = Run(batch, run_kind)
608
def create_tree(in_dir, _levels, _spread):
614
for i in range(_spread):
616
fn = j(in_dir, next_unique_basename('file'))
618
f.write('This is %s\n' % fn)
623
dn = j(in_dir, next_unique_basename('dir'))
624
create_tree(dn, _levels - 1, _spread)
630
cmd.extend( list(args) )
632
print 'svn cmd:', ' '.join(cmd)
636
stdin_arg = subprocess.PIPE
642
p = subprocess.Popen(cmd,
644
stdout=subprocess.PIPE,
645
stderr=subprocess.PIPE,
647
stdout,stderr = p.communicate(input=stdin)
649
stdout = stderr = None
655
print "STDOUT: [[[\n%s]]]" % ''.join(stdout)
657
print "STDERR: [[[\n%s]]]" % ''.join(stderr)
663
return svn('add', *args)
666
return svn('commit', '-mm', *args)
669
return svn('update', *args)
672
return svn('status', *args)
675
return svn('info', *args)
677
_chars = [chr(x) for x in range(ord('a'), ord('z') +1)]
680
return ''.join( [random.choice(_chars) for i in range(len)] )
683
dest = next_unique_basename(path + '_copied')
684
svn('copy', path, dest)
687
dest = path + '_moved'
688
svn('move', path, dest)
691
so, se = svn('proplist', path)
692
propnames = [line.strip() for line in so.strip().split('\n')[1:]]
696
svn('ps', propnames[len(propnames) / 2], randstr(), path)
699
if len(propnames) > 1:
700
svn('propdel', propnames[len(propnames) / 2], path)
704
svn('propset', randstr(), randstr(), path)
707
if os.path.isdir(path):
712
f.write('\n%s\n' % randstr())
716
if os.path.isfile(path):
719
if random.choice((True, False)):
721
svn('mkdir', j(path, next_unique_basename('new_dir')))
724
new_path = j(path, next_unique_basename('new_file'))
725
f = open(new_path, 'w')
733
_mod_funcs = (_mod, _add, _propmod, _propadd, )#_copy,) # _move, _del)
735
def modify_tree(in_dir, fraction):
736
child_names = os.listdir(in_dir)
737
for child_name in child_names:
738
if child_name[0] == '.':
740
if random.random() < fraction:
741
path = j(in_dir, child_name)
742
random.choice(_mod_funcs)(path)
744
for child_name in child_names:
745
if child_name[0] == '.': continue
746
path = j(in_dir, child_name)
747
if os.path.isdir(path):
748
modify_tree(path, fraction)
750
def propadd_tree(in_dir, fraction):
751
for child_name in os.listdir(in_dir):
752
if child_name[0] == '.': continue
753
path = j(in_dir, child_name)
754
if random.random() < fraction:
756
if os.path.isdir(path):
757
propadd_tree(path, fraction)
760
def rmtree_onerror(func, path, exc_info):
761
"""Error handler for ``shutil.rmtree``.
763
If the error is due to an access error (read only file)
764
it attempts to add write permission and then retries.
766
If the error is for another reason it re-raises the error.
768
Usage : ``shutil.rmtree(path, onerror=onerror)``
770
if not os.access(path, os.W_OK):
771
# Is the error an access error ?
772
os.chmod(path, stat.S_IWUSR)
777
base = tempfile.mkdtemp()
779
# ensure identical modifications for every run
785
repos = j(base, 'repos')
786
repos = repos.replace('\\', '/')
790
if repos.startswith('/'):
791
file_url = 'file://%s' % repos
793
file_url = 'file:///%s' % repos
795
print '\nRunning svn benchmark in', base
796
print 'dir levels: %s; new files and dirs per leaf: %s' %(
797
run_kind.levels, run_kind.spread)
799
started = datetime.datetime.now()
802
run_cmd([svnadmin_bin, 'create', repos])
803
svn('checkout', file_url, wc)
805
trunk = j(wc, 'trunk')
806
create_tree(trunk, run_kind.levels, run_kind.spread)
811
propadd_tree(trunk, 0.05)
817
trunk_url = file_url + '/trunk'
818
branch_url = file_url + '/branch'
820
svn('copy', '-mm', trunk_url, branch_url)
827
svn('checkout', trunk_url, wc2)
829
modify_tree(wc2, 0.5)
835
svn('switch', branch_url, wc2)
836
modify_tree(wc2, 0.5)
843
modify_tree(trunk, 0.5)
849
svn('merge', '--accept=postpone', trunk_url, wc2)
852
svn('resolve', '--accept=mine-conflict', wc2)
854
svn('resolved', '-R', wc2)
861
svn('merge', '--accept=postpone', '--reintegrate', branch_url, trunk)
863
svn('resolve', '--accept=mine-conflict', wc)
865
svn('resolved', '-R', wc)
871
svn('delete', j(wc, 'branch'))
878
stopped = datetime.datetime.now()
879
print '\nDone with svn benchmark in', (stopped - started)
881
run.remember_timing(TOTAL_RUN,
882
timedelta_to_seconds(stopped - started))
886
shutil.rmtree(base, onerror=rmtree_onerror)
891
# ---------------------------------------------------------------------
894
def cmdline_run(db, options, run_kind_str, N=1):
895
run_kind = parse_one_timing_selection(db, run_kind_str)
899
print 'Hi, going to run a Subversion benchmark series of %d runs...' % N
900
print 'Label is %s' % run_kind.label()
902
# can we run the svn binaries?
903
svn_bin = j(options.svn_bin_dir, 'svn')
904
svnadmin_bin = j(options.svn_bin_dir, 'svnadmin')
906
for b in (svn_bin, svnadmin_bin):
907
so,se = run_cmd([b, '--version'])
909
bail("Can't run %s" % b)
911
print ', '.join([s.strip() for s in so.split('\n')[:2]])
916
print 'Run %d of %d' % (i + 1, N)
917
perform_run(batch, run_kind,
918
svn_bin, svnadmin_bin, options.verbose)
923
def cmdline_list(db, options, *args):
924
run_kinds = parse_timings_selections(db, *args)
926
for run_kind in run_kinds:
929
def add_if_not_none(name, val):
931
constraints.append(' %s = %s' % (name, val))
932
add_if_not_none('branch', run_kind.branch)
933
add_if_not_none('revision', run_kind.revision)
934
add_if_not_none('levels', run_kind.levels)
935
add_if_not_none('spread', run_kind.spread)
937
print 'For\n', '\n'.join(constraints)
940
d = TimingQuery(db, run_kind)
942
cmd_names = d.get_sorted_command_names()
944
print '\n%d command names:\n ' % len(cmd_names), '\n '.join(cmd_names)
946
branches = d.get_sorted_branches()
947
if branches and (len(branches) > 1 or branches[0] != run_kind.branch):
948
print '\n%d branches:\n ' % len(branches), '\n '.join(branches)
950
revisions = d.get_sorted_revisions()
951
if revisions and (len(revisions) > 1 or revisions[0] != run_kind.revision):
952
print '\n%d revisions:\n ' % len(revisions), '\n '.join(revisions)
954
levels_spread = d.get_sorted_levels_spread()
955
if levels_spread and (
956
len(levels_spread) > 1
957
or levels_spread[0] != (run_kind.levels, run_kind.spread)):
958
print '\n%d kinds of levels x spread:\n ' % len(levels_spread), '\n '.join(
959
[ ('%dx%d' % (l, s)) for l,s in levels_spread ])
961
print "\n%d runs in %d batches.\n" % (d.count_runs_batches())
964
def cmdline_show(db, options, *run_kind_strings):
965
run_kinds = parse_timings_selections(db, *run_kind_strings)
966
for run_kind in run_kinds:
967
q = TimingQuery(db, run_kind)
968
timings = q.get_timings()
123
s.append('Timings for %s' % self.name)
124
s.append(' N min max avg operation (unit is seconds)')
126
names = sorted(self.timings.keys())
129
timings = self.timings.get(name)
130
if not name or not timings: continue
132
tmin, tmax, tavg = self.min_max_avg(name)
134
s.append('%5d %7.2f %7.2f %7.2f %s' % (
971
s.append('Timings for %s' % run_kind.label())
972
s.append(' N min max avg operation (unit is seconds)')
974
for command_name in q.get_sorted_command_names():
975
if options.command_names and command_name not in options.command_names:
977
n, tmin, tmax, tavg = timings[command_name]
979
s.append('%4s %7.2f %7.2f %7.2f %s' % (
144
def compare_to(self, other):
147
return float(a) / float(b)
989
def cmdline_compare(db, options, *args):
990
run_kinds = parse_timings_selections(db, *args)
991
if len(run_kinds) < 2:
992
bail("Need at least two sets of timings to compare.")
995
left_kind = run_kinds[0]
996
leftq = TimingQuery(db, left_kind)
997
left = leftq.get_timings()
999
bail("No timings for %s" % left_kind.label())
1001
for run_kind_idx in range(1, len(run_kinds)):
1002
right_kind = run_kinds[run_kind_idx]
1004
rightq = TimingQuery(db, right_kind)
1005
right = rightq.get_timings()
1007
print "No timings for %s" % right_kind.label()
1010
label = 'Compare %s to %s' % (right_kind.label(), left_kind.label())
1014
verbose = options.verbose
1016
s.append(' N avg operation')
1018
s.append(' N min max avg operation')
1020
command_names = [name for name in leftq.get_sorted_command_names()
1022
if options.command_names:
1023
command_names = [name for name in command_names
1024
if name in options.command_names]
1026
for command_name in command_names:
1027
left_N, left_min, left_max, left_avg = left[command_name]
1028
right_N, right_min, right_max, right_avg = right[command_name]
1030
N_str = '%s/%s' % (n_label(left_N), n_label(right_N))
1031
avg_str = '%7.2f|%+7.3f' % (do_div(left_avg, right_avg),
1032
do_diff(left_avg, right_avg))
1035
s.append('%9s %-16s %s' % (N_str, avg_str, command_name))
152
return float(a) - float(b)
157
othername = other.name
159
othername = 'the other'
161
selftotal = self.min_max_avg(TOTAL_RUN)[2]
162
othertotal = other.min_max_avg(TOTAL_RUN)[2]
164
s = ['COMPARE %s to %s' % (othername, selfname)]
166
if TOTAL_RUN in self.timings and TOTAL_RUN in other.timings:
167
s.append(' %s times: %5.1f seconds avg for %s' % (TOTAL_RUN,
168
othertotal, othername))
169
s.append(' %s %5.1f seconds avg for %s' % (' ' * len(TOTAL_RUN),
170
selftotal, selfname))
173
s.append(' min max avg operation')
175
names = sorted(self.timings.keys())
178
if not name in other.timings:
182
min_me, max_me, avg_me = self.min_max_avg(name)
183
min_other, max_other, avg_other = other.min_max_avg(name)
185
s.append('%-16s %-16s %-16s %s' % (
187
do_div(min_me, min_other),
188
do_diff(min_me, min_other)
192
do_div(max_me, max_other),
193
do_diff(max_me, max_other)
197
do_div(avg_me, avg_other),
198
do_diff(avg_me, avg_other)
1037
min_str = '%7.2f|%+7.3f' % (do_div(left_min, right_min),
1038
do_diff(left_min, right_min))
1039
max_str = '%7.2f|%+7.3f' % (do_div(left_max, right_max),
1040
do_diff(left_max, right_max))
1042
s.append('%9s %-16s %-16s %-16s %s' % (N_str, min_str, max_str, avg_str,
204
'("1.23|+0.45" means factor=1.23, difference in seconds = 0.45',
205
'factor < 1 or difference < 0 means \'%s\' is faster than \'%s\')'
206
% (self.name, othername)])
211
def add(self, other):
212
for name, other_times in other.timings.items():
213
my_times = self.timings.get(name)
216
self.timings[name] = my_times
217
my_times.extend(other_times)
226
def next_name(prefix):
229
return '_'.join((prefix, str(_create_count)))
231
def create_tree(in_dir, levels, spread=5):
237
for i in range(spread):
239
fn = j(in_dir, next_name('file'))
241
f.write('This is %s\n' % fn)
246
dn = j(in_dir, next_name('dir'))
247
create_tree(dn, levels - 1, spread)
253
### options comes from the global namespace; it should be passed
254
cmd = [options.svn] + list(args)
256
print 'svn cmd:', ' '.join(cmd)
260
stdin_arg = subprocess.PIPE
264
### timings comes from the global namespace; it should be passed
267
p = subprocess.Popen(cmd,
269
stdout=subprocess.PIPE,
270
stderr=subprocess.PIPE,
272
stdout,stderr = p.communicate(input=stdin)
274
stdout = stderr = None
280
print "STDOUT: [[[\n%s]]]" % ''.join(stdout)
282
print "STDERR: [[[\n%s]]]" % ''.join(stderr)
288
return svn('add', *args)
291
return svn('commit', '-mm', *args)
294
return svn('update', *args)
297
return svn('status', *args)
299
_chars = [chr(x) for x in range(ord('a'), ord('z') +1)]
302
return ''.join( [random.choice(_chars) for i in range(len)] )
305
dest = next_name(path + '_copied')
306
svn('copy', path, dest)
309
dest = path + '_moved'
310
svn('move', path, dest)
313
so, se = svn('proplist', path)
314
propnames = [line.strip() for line in so.strip().split('\n')[1:]]
318
svn('ps', propnames[len(propnames) / 2], randstr(), path)
321
if len(propnames) > 1:
322
svn('propdel', propnames[len(propnames) / 2], path)
327
svn('propset', randstr(), randstr(), path)
331
if os.path.isdir(path):
332
return _propmod(path)
335
f.write('\n%s\n' % randstr())
339
if os.path.isfile(path):
342
if random.choice((True, False)):
344
svn('mkdir', j(path, next_name('new_dir')))
347
new_path = j(path, next_name('new_file'))
348
f = open(new_path, 'w')
356
_mod_funcs = (_mod, _add, _propmod, _propadd, )#_copy,) # _move, _del)
358
def modify_tree(in_dir, fraction):
359
child_names = os.listdir(in_dir)
360
for child_name in child_names:
361
if child_name[0] == '.':
363
if random.random() < fraction:
364
path = j(in_dir, child_name)
365
random.choice(_mod_funcs)(path)
367
for child_name in child_names:
368
if child_name[0] == '.': continue
369
path = j(in_dir, child_name)
370
if os.path.isdir(path):
371
modify_tree(path, fraction)
373
def propadd_tree(in_dir, fraction):
374
for child_name in os.listdir(in_dir):
375
if child_name[0] == '.': continue
376
path = j(in_dir, child_name)
377
if random.random() < fraction:
379
if os.path.isdir(path):
380
propadd_tree(path, fraction)
383
def rmtree_onerror(func, path, exc_info):
384
"""Error handler for ``shutil.rmtree``.
386
If the error is due to an access error (read only file)
387
it attempts to add write permission and then retries.
389
If the error is for another reason it re-raises the error.
391
Usage : ``shutil.rmtree(path, onerror=onerror)``
393
if not os.access(path, os.W_OK):
394
# Is the error an access error ?
395
os.chmod(path, stat.S_IWUSR)
401
def run(levels, spread, N):
403
base = tempfile.mkdtemp()
405
# ensure identical modifications for every run
409
repos = j(base, 'repos')
410
repos = repos.replace('\\', '/')
414
if repos.startswith('/'):
415
file_url = 'file://%s' % repos
1046
'(legend: "1.23|+0.45" means: slower by factor 1.23 and by 0.45 seconds;',
1047
' factor < 1 and seconds < 0 means \'%s\' is faster.'
1048
% right_kind.label(),
1049
' "2/3" means: \'%s\' has 2 timings on record, the other has 3.)'
1057
# ------------------------------------------------------- charts
1059
def cmdline_chart_compare(db, options, *args):
1061
matplotlib.use('Agg')
1063
import matplotlib.pylab as plt
1067
command_names = None
1069
run_kinds = parse_timings_selections(db, *args)
1071
# iterate the timings selections and accumulate data
1072
for run_kind in run_kinds:
1073
query = TimingQuery(db, run_kind)
1074
timings = query.get_timings()
1076
print "No timings for %s" % run_kind.label()
1078
labels.append(run_kind.label())
1079
timing_sets.append(timings)
1081
# it only makes sense to compare those commands that have timings
1082
# in the first selection, because that is the one everything else
1083
# is compared to. Remember the first selection's command names.
1084
if not command_names:
1085
command_names = query.get_sorted_command_names()
1088
if len(timing_sets) < 2:
1089
bail("Not enough timings")
1091
if options.command_names:
1092
command_names = [name for name in command_names
1093
if name in options.command_names]
1095
chart_path = options.chart_path
1097
chart_path = 'compare_' + '_'.join(
1098
[ filesystem_safe_string(l) for l in labels ]
1101
N = len(command_names)
1102
M = len(timing_sets) - 1
1106
group_positions = np.arange(N) # the y locations for the groups
1107
dist = 1. / (1. + M)
1108
height = (1. - dist) / M # the height of the bars
1110
fig = plt.figure(figsize=(12, 5 + 0.2*N*M))
1111
plot1 = fig.add_subplot(121)
1112
plot2 = fig.add_subplot(122)
1114
left = timing_sets[0]
1116
# Iterate timing sets. Each loop produces one bar for each command name
1118
for label_i,label in enumerate(labels[1:],1):
1119
right = timing_sets[label_i]
1123
for cmd_i, command_name in enumerate(command_names):
1124
if command_name not in right:
1128
left_N, left_min, left_max, left_avg = left[command_name]
1129
right_N, right_min, right_max, right_avg = right[command_name]
1131
div_avg = 100. * (do_div(left_avg, right_avg) - 1.0)
417
file_url = 'file:///%s' % repos
419
so, se = svn('--version')
421
print "Can't find svn."
423
version = ', '.join([s.strip() for s in so.split('\n')[:2]])
425
print '\nRunning svn benchmark in', base
426
print 'dir levels: %s; new files and dirs per leaf: %s; run %d of %d' %(
427
levels, spread, i + 1, N)
430
started = datetime.datetime.now()
433
run_cmd(['svnadmin', 'create', repos])
434
svn('checkout', file_url, wc)
436
trunk = j(wc, 'trunk')
437
create_tree(trunk, levels, spread)
442
propadd_tree(trunk, 0.5)
447
trunk_url = file_url + '/trunk'
448
branch_url = file_url + '/branch'
450
svn('copy', '-mm', trunk_url, branch_url)
456
svn('checkout', trunk_url, wc2)
458
modify_tree(wc2, 0.5)
464
svn('switch', branch_url, wc2)
465
modify_tree(wc2, 0.5)
471
modify_tree(trunk, 0.5)
477
svn('merge', '--accept=postpone', trunk_url, wc2)
479
svn('resolve', '--accept=mine-conflict', wc2)
481
svn('resolved', '-R', wc2)
487
svn('merge', '--accept=postpone', '--reintegrate', branch_url, trunk)
489
svn('resolve', '--accept=mine-conflict', wc)
491
svn('resolved', '-R', wc)
497
svn('delete', j(wc, 'branch'))
504
stopped = datetime.datetime.now()
505
print '\nDone with svn benchmark in', (stopped - started)
507
### timings comes from the global namespace; it should be passed
508
timings.submit_timing(TOTAL_RUN,
509
timedelta_to_seconds(stopped - started))
511
# rename ps to prop mod
512
if timings.timings.get('ps'):
513
has = timings.timings.get('prop mod')
516
timings.timings['prop mod'] = has
517
has.extend( timings.timings['ps'] )
518
del timings.timings['ps']
520
print timings.summary()
522
shutil.rmtree(base, onerror=rmtree_onerror)
525
def read_from_file(file_path):
526
f = open(file_path, 'rb')
528
instance = cPickle.load(f)
529
instance.name = os.path.basename(file_path)
535
def write_to_file(file_path, instance):
536
f = open(file_path, 'wb')
537
cPickle.dump(instance, f)
540
def cmd_compare(path1, path2):
541
t1 = read_from_file(path1)
542
t2 = read_from_file(path2)
548
print t2.compare_to(t1)
550
def cmd_combine(dest, *paths):
551
total = Timings('--version');
554
t = read_from_file(path)
557
print total.summary()
558
write_to_file(dest, total)
560
def cmd_run(timings_path, levels, spread, N=1):
565
print '\n\nHi, going to run a Subversion benchmark series of %d runs...' % N
567
### UGH! should pass to run()
570
if os.path.isfile(timings_path):
571
print 'Going to add results to existing file', timings_path
572
timings = read_from_file(timings_path)
574
print 'Going to write results to new file', timings_path
575
timings = Timings('--version')
577
run(levels, spread, N)
579
write_to_file(timings_path, timings)
581
def cmd_show(*paths):
582
for timings_path in paths:
583
timings = read_from_file(timings_path)
584
print '---\n%s' % timings_path
585
print timings.summary()
1137
diff_val = do_diff(left_avg, right_avg)
1139
ofs = (dist + height) / 2. + height * (label_i - 1)
1141
barheight = height * (1.0 - dist)
1143
y = float(cmd_i) + ofs
1148
color=col, edgecolor='white')
1149
plot1.text(0., y + height/2.,
1150
'%s %+5.1f%%' % (label, div_avg),
1151
ha='right', va='center', size='small',
1152
rotation=0, family='monospace')
1157
color=col, edgecolor='white')
1158
plot2.text(0., y + height/2.,
1159
'%s %+6.2fs' % (label, diff_val),
1160
ha='right', va='center', size='small',
1161
rotation=0, family='monospace')
1164
for p in (plot1, plot2):
1165
xlim = list(p.get_xlim())
1168
# make sure the zero line is far enough right so that the annotations
1169
# fit inside the chart. About half the width should suffice.
1170
if xlim[0] > -xlim[1]:
1174
p.set_yticks(group_positions + (height / 2.))
1175
p.set_yticklabels(())
1176
p.set_ylim((len(command_names), 0))
1179
plot1.set_xticklabels(('+-0%',), rotation=0)
1180
plot1.set_title('Average runtime change from %s in %%' % labels[0],
1183
plot2.set_xticklabels(('+-0s',), rotation=0)
1184
plot2.set_title('Average runtime change from %s in seconds' % labels[0],
1187
margin = 1./(2 + N*M)
1190
titlemargin = margin * 1.5
1192
fig.subplots_adjust(left=0.005, right=0.995, wspace=0.3, bottom=margin,
1193
top=1.0-margin-titlemargin)
1195
ystep = (1.0 - 2.*margin - titlemargin) / len(command_names)
1197
for idx,command_name in enumerate(command_names):
1198
ylabel = '%s\nvs. %.1fs' % (
1200
left[command_name][3])
1202
ypos=1.0 - margin - titlemargin - ystep/M - ystep * idx
1203
plt.figtext(0.5, ypos,
1205
ha='center', va='top',
1206
size='medium', weight='bold')
1207
plt.figtext(0.5, ypos - ystep/(M+1),
1209
labels[0], left[command_name][3]),
1210
ha='center', va='top',
1214
plt.figtext(0.5, 1. - titlemargin/2, options.title, ha='center',
1215
va='center', weight='bold')
1217
plt.savefig(chart_path)
1218
print 'wrote chart file:', chart_path
1221
# ------------------------------------------------------------ main
1224
# Custom option formatter, keeping newlines in the description.
1226
# http://groups.google.com/group/comp.lang.python/msg/09f28e26af0699b1
1228
class IndentedHelpFormatterWithNL(optparse.IndentedHelpFormatter):
1229
def format_description(self, description):
1230
if not description: return ""
1231
desc_width = self.width - self.current_indent
1232
indent = " "*self.current_indent
1233
bits = description.split('\n')
1237
initial_indent=indent,
1238
subsequent_indent=indent)
1240
result = "\n".join(formatted_bits) + "\n"
591
1243
if __name__ == '__main__':
592
parser = optparse.OptionParser()
1244
parser = optparse.OptionParser(formatter=IndentedHelpFormatterWithNL())
593
1245
# -h is automatically added.
594
1246
### should probably expand the help for that. and see about -?
595
1247
parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
596
1248
help='Verbose operation')
597
parser.add_option('--svn', action='store', dest='svn', default='svn',
598
help='Specify Subversion executable to use')
600
### should start passing this, but for now: make it global
1249
parser.add_option('-b', '--svn-bin-dir', action='store', dest='svn_bin_dir',
1251
help='Specify directory to find Subversion binaries in')
1252
parser.add_option('-f', '--db-path', action='store', dest='db_path',
1253
default='benchmark.db',
1254
help='Specify path to SQLite database file')
1255
parser.add_option('-o', '--chart-path', action='store', dest='chart_path',
1256
help='Supply a path for chart output.')
1257
parser.add_option('-c', '--command-names', action='store',
1258
dest='command_names',
1259
help='Comma separated list of command names to limit to.')
1260
parser.add_option('-t', '--title', action='store',
1262
help='For charts, a title to print in the chart graphics.')
1264
parser.set_description(__doc__)
1265
parser.set_usage('')
603
1268
options, args = parser.parse_args()
1270
def usage(msg=None):
605
1277
# there should be at least one arg left: the sub-command
1279
usage('No command argument supplied.')
619
elif cmd == 'combine':
626
if len(args) < 3 or len(args) > 4:
1284
db = TimingsDb(options.db_path)
1287
if len(args) < 1 or len(args) > 2:
1289
cmdline_run(db, options, *args)
1291
elif cmd == 'compare':
1294
cmdline_compare(db, options, *args)
1297
cmdline_list(db, options, *args)
631
1299
elif cmd == 'show':
1300
cmdline_show(db, options, *args)
1302
elif cmd == 'chart':
1303
if 'compare'.startswith(args[0]):
1304
cmdline_chart_compare(db, options, *args[1:])
1309
usage('Unknown subcommand argument: %s' % cmd)