← Back to benchmarkcompare_stores.py
Snapshot of local benchmark script · SHA-256 f74fddaf7e024d29d969fe975e8df5a55652094a18461cb38163f5e5448c8f22#!/usr/bin/env python3
"""Compare actual version-store CLI writes, retrievals and storage on shared histories."""
import argparse
import hashlib
import itertools
import json
import os
import platform
import random
import shutil
import statistics
import subprocess
import tempfile
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent
SEED = 20261028
NAMES = ('git', 'fossil', 'nab', 'dnab', 'rcs')
def digest(data):
return hashlib.sha256(data).hexdigest()
def tree_bytes(path):
return sum(p.stat().st_size for p in path.rglob('*') if p.is_file())
def summarize(samples):
ordered = sorted(samples)
return {'median_ms': statistics.median(samples), 'min_ms': min(samples),
'max_ms': max(samples), 'p95_ms': ordered[min(len(ordered) - 1, int(len(ordered) * .95))],
'samples_ms': samples}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--versions', type=int, default=1000)
parser.add_argument('--sizes', type=int, nargs='+', default=[4096, 131072])
parser.add_argument('--workloads', nargs='+', choices=('binary', 'text'), default=['binary', 'text'])
parser.add_argument('--runs', type=int, default=1)
parser.add_argument('--reads', type=int, default=3)
parser.add_argument('--tools', nargs='+', choices=NAMES, default=list(NAMES))
args = parser.parse_args()
if args.versions < 1 or args.runs < 1 or args.reads < 1 or any(s < 32 for s in args.sizes):
parser.error('versions, runs and reads must be positive; sizes must be at least 32')
if any(len(set(items)) != len(items) for items in (args.tools, args.sizes, args.workloads)):
parser.error('tools, sizes and workloads must not be duplicated')
needed = {'git': ['git'], 'fossil': ['fossil'], 'nab': ['nab'], 'dnab': ['dnab'], 'rcs': ['rcs', 'ci', 'co']}
binaries = {}
for name in args.tools:
for binary in needed[name]:
path = shutil.which(binary)
if not path:
parser.error(f'{binary} not found; select installed tools with --tools')
binaries[binary] = str(Path(path).resolve())
directory = Path(tempfile.mkdtemp(prefix='compare-stores-', dir=ROOT))
home = directory / 'home'
home.mkdir()
env = os.environ.copy()
env.update(HOME=str(home), GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull,
GIT_TERMINAL_PROMPT='0', LC_ALL='C', TZ='UTC', USER='benchmark')
for key in list(env):
if key.startswith('GIT_') and key not in ('GIT_CONFIG_NOSYSTEM', 'GIT_CONFIG_GLOBAL', 'GIT_TERMINAL_PROMPT'):
del env[key]
def call(command, cwd, timed=False):
start = time.perf_counter_ns()
result = subprocess.run(command, cwd=cwd, env=env, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stdin=subprocess.DEVNULL, timeout=300)
elapsed = (time.perf_counter_ns() - start) / 1e6
if result.returncode:
raise RuntimeError(f'{command!r} failed ({result.returncode}): {result.stderr.decode(errors="replace")}\n{result.stdout.decode(errors="replace")}')
return (elapsed if timed else None), result.stdout
def cmd(binary, *items):
return [binaries[binary], *map(str, items)]
report = {'seed': SEED, 'platform': platform.platform(), 'arguments': vars(args),
'script_sha256': digest(Path(__file__).read_bytes()),
'binaries': {n: {'path': p, 'sha256': digest(Path(p).read_bytes())} for n, p in binaries.items()},
'directory': str(directory), 'completed': False, 'cases': [],
'protocol': ('Fresh isolated repositories per trial; deterministic identical cumulative binary and ASCII text histories. '
'Each edit replaces three 16-byte regions and appends one byte. Text retains line breaks in replacement regions. Tools rotate each revision and read repeat. '
'Write timing includes CLI startup and normal storage operations, including git add+commit and initial fossil add+commit; '
'fixture generation and working-file writes are excluded. RCS uses binary keyword mode and retains a lock. '
'Fossil commits carry unique tags for retrieval; tags add metadata. Git automatic maintenance is disabled. '
'Read timing captures reconstructed stdout; SHA-256 checking is outside the timed interval. '
'All revisions are additionally reconstructed and verified outside timed probes. '
'Sizes are logical bytes in repository files only, excluding working copies and checkout databases. '
'Packed git is a copied repository repacked separately; its copy and repack do not affect normal writes. '
'Initialization, verification and packing are outside write timings. Warm caches and background load are uncontrolled.'),
'limitations': ('CLI/process, metadata, compression, locking and durability policies differ; this is an end-to-end '
'single-file comparison, not a measurement of a delta algorithm alone. Git and Fossil track commits as well as contents; '
'nab/dnab and RCS are file archives. Git packing time is reported separately; packed size is not the normal-write size.')}
result_path = directory / 'results.json'
def save():
result_path.write_text(json.dumps(report, indent=2) + '\n')
save()
checkpoints = sorted({n for n in (100, 1000, args.versions) if n <= args.versions})
for size, workload in itertools.product(args.sizes, args.workloads):
for trial in range(args.runs):
case_dir = directory / f'{workload}-{size}-{trial}'
case_dir.mkdir()
dirs = {}
tools = {}
for name in args.tools:
folder = case_dir / name
folder.mkdir()
dirs[name] = folder
tools[name] = {'write_ms': [], 'checkpoints': []}
if name == 'git':
call(cmd('git', 'init', '-q', '.'), folder)
for key, value in [('user.name', 'Benchmark'), ('user.email', 'benchmark@example.invalid'),
('gc.auto', '0'), ('commit.gpgsign', 'false'), ('core.hooksPath', os.devnull)]:
call(cmd('git', 'config', key, value), folder)
elif name == 'fossil':
call(cmd('fossil', 'init', 'repo.fossil', '--admin-user', 'benchmark'), folder)
checkout = folder / 'checkout'
checkout.mkdir()
call(cmd('fossil', 'open', folder / 'repo.fossil', '--nosync'), checkout)
dirs[name] = checkout
elif name == 'rcs':
call(cmd('rcs', '-q', '-i', '-kb', '-t-benchmark', 'content,v'), folder)
case = {'workload': workload, 'initial_bytes': size, 'trial': trial, 'fixture_sha256': [], 'tools': tools}
report['cases'].append(case)
rng = random.Random(SEED + size)
if workload == 'binary':
data = bytearray(rng.randbytes(size))
else:
raw = rng.randbytes(size).hex()
data = bytearray('\n'.join(raw[i:i + 64] for i in range(0, len(raw), 64))[:size], 'ascii')
hashes = case['fixture_sha256']
def get(name, version, git_ids):
folder = dirs[name]
if name == 'git':
command = cmd('git', 'show', f'{git_ids[version]}:content')
elif name == 'fossil':
command = cmd('fossil', 'cat', 'content', '-r', f'bench-{version}')
elif name in ('nab', 'dnab'):
command = cmd(name, 'rebuild', folder / f'archive.{name}', version)
else:
command = cmd('co', '-q', '-p1.' + str(version + 1), '-kb', 'content,v')
return call(command, folder, timed=True)
for version in range(args.versions):
if version:
for _ in range(3):
at = rng.randrange(len(data) - 16)
if workload == 'text':
while b'\n' in data[at:at + 16]:
at = rng.randrange(len(data) - 16)
data[at:at + 16] = rng.randbytes(8).hex().encode('ascii')
else:
data[at:at + 16] = rng.randbytes(16)
data += (rng.randbytes(1).hex()[:1].encode('ascii') if workload == 'text'
else rng.randbytes(1))
hashes.append(digest(data))
if version and hashes[-1] in hashes[:-1]:
raise RuntimeError('fixture contains a duplicate version')
order = args.tools[:]
shift = (version + trial) % len(order)
order = order[shift:] + order[:shift]
for name in order:
folder = dirs[name]
(folder / 'content').write_bytes(data)
elapsed = 0
if name == 'git':
ms, _ = call(cmd('git', 'add', '--', 'content'), folder, True)
elapsed += ms
ms, _ = call(cmd('git', 'commit', '-q', '-m', f'version {version}', '--no-verify'), folder, True)
elif name == 'fossil':
if version == 0:
ms, _ = call(cmd('fossil', 'add', 'content'), folder, True)
elapsed += ms
ms, _ = call(cmd('fossil', 'commit', '-m', f'version {version}', '--tag', f'bench-{version}',
'--no-warnings', '--no-verify', '--nosign', '--nosync', '--user-override', 'benchmark', '--hash'), folder, True)
elif name in ('nab', 'dnab'):
ms, _ = call(cmd(name, 'patch', folder / 'content', folder / f'archive.{name}'), folder, True)
else:
ms, _ = call(cmd('ci', '-q', '-l', '-mbenchmark', 'content'), folder, True)
tools[name]['write_ms'].append(elapsed + ms)
count = version + 1
if count not in checkpoints:
continue
git_ids = []
if 'git' in args.tools:
_, output = call(cmd('git', 'rev-list', '--reverse', 'HEAD'), dirs['git'])
git_ids = output.decode().splitlines()
if len(git_ids) != count:
raise RuntimeError('git revision count mismatch')
probes = {name: {label: [] for label in ('first', 'middle', 'latest')} for name in args.tools}
for repeat in range(args.reads):
shift = (repeat + trial + count) % len(args.tools)
order = args.tools[shift:] + args.tools[:shift]
for name in order:
for label, index in [('first', 0), ('middle', (count - 1) // 2), ('latest', count - 1)]:
ms, output = get(name, index, git_ids)
if digest(output) != hashes[index]:
raise RuntimeError(f'{name}: incorrect reconstruction of revision {index}')
probes[name][label].append(ms)
for name in args.tools:
folder = dirs[name]
if name == 'git':
repo_size = tree_bytes(folder / '.git')
elif name == 'fossil':
repo_size = (folder.parent / 'repo.fossil').stat().st_size
elif name == 'rcs':
repo_size = (folder / 'content,v').stat().st_size
else:
repo_size = (folder / f'archive.{name}').stat().st_size
entry = {'versions': count, 'repository_logical_bytes': repo_size,
'write_last_100': summarize(tools[name]['write_ms'][-100:]),
'cumulative_write_ms': sum(tools[name]['write_ms']),
'reads': {label: summarize(samples) for label, samples in probes[name].items()}}
if name == 'git':
packed = case_dir / 'packed.git'
shutil.copytree(folder / '.git', packed)
ms, _ = call(cmd('git', '--git-dir', packed, 'repack', '-ad'), case_dir, True)
ms2, _ = call(cmd('git', '--git-dir', packed, 'prune-packed'), case_dir, True)
entry['packed_repository_logical_bytes'] = tree_bytes(packed)
entry['pack_ms'] = ms + ms2
shutil.rmtree(packed)
tools[name]['checkpoints'].append(entry)
save()
print(f'{workload}, {size} bytes, trial {trial + 1}, {count} versions complete', flush=True)
for name in args.tools:
for version in range(args.versions):
_, output = get(name, version, git_ids)
if digest(output) != hashes[version]:
raise RuntimeError(f'{name}: incorrect reconstruction of revision {version}')
tools[name]['verified_revisions'] = args.versions
save()
report['completed'] = True
save()
print(f'Results: {result_path}', flush=True)
if __name__ == '__main__':
main()