aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/005-storage-edit-latency/measure.py
diff options
context:
space:
mode:
Diffstat (limited to 'research/experiments/005-storage-edit-latency/measure.py')
-rw-r--r--research/experiments/005-storage-edit-latency/measure.py99
1 files changed, 99 insertions, 0 deletions
diff --git a/research/experiments/005-storage-edit-latency/measure.py b/research/experiments/005-storage-edit-latency/measure.py
new file mode 100644
index 0000000..b390c92
--- /dev/null
+++ b/research/experiments/005-storage-edit-latency/measure.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""Time the same durable one-MiB document creation and one-byte revision edit."""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import platform
+import statistics
+import subprocess
+import tempfile
+import time
+
+HERE = Path(__file__).resolve().parents[3]
+PROGRAM = HERE / 'prototypes/connections/examples/editor.pure'
+INITIAL = b'A' * 1_048_576
+EXPECTED = INITIAL[:-1] + b'Z'
+
+
+def invoke(command):
+ started = time.monotonic_ns()
+ result = subprocess.run([str(arg) for arg in command], capture_output=True, timeout=30)
+ elapsed_ms = (time.monotonic_ns() - started) / 1_000_000
+ if result.returncode:
+ raise AssertionError((command, result.returncode, result.stderr[-1000:]))
+ return result, elapsed_ms
+
+
+def one(binary, root, initial, replacement):
+ base = [binary, '--program', PROGRAM, '--store', root]
+ created, create_ms = invoke([*base, 'edit', 'document', 0, 0, 0, initial])
+ assert json.loads(created.stdout)['revision'] == '1'
+ edited, edit_ms = invoke([*base, 'edit', 'document', 1, 1_048_575, 1, replacement])
+ assert json.loads(edited.stdout)['revision'] == '2'
+ shown, _ = invoke([*base, 'show', 'document'])
+ assert shown.stdout == EXPECTED
+ assert json.loads(shown.stderr)['revision'] == '2'
+ files = [path for path in root.iterdir() if path.is_file()]
+ return {'create_ms': round(create_ms, 3), 'edit_ms': round(edit_ms, 3),
+ 'store_files': len(files),
+ 'store_file_bytes': sum(path.stat().st_size for path in files),
+ 'store_allocated_bytes': sum(path.stat().st_blocks * 512 for path in files)}
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--flat', type=Path, required=True)
+ parser.add_argument('--copy', type=Path, required=True)
+ parser.add_argument('--paged', type=Path, required=True)
+ parser.add_argument('--scratch-parent', type=Path, required=True)
+ parser.add_argument('--trials', type=int, default=18)
+ parser.add_argument('--output', type=Path, required=True)
+ args = parser.parse_args()
+ if args.trials < 3 or args.trials % 3 or args.output.exists():
+ parser.error('trials must be a positive multiple of three and output must be new')
+ variants = {'flat': args.flat.resolve(), 'copy': args.copy.resolve(),
+ 'paged': args.paged.resolve()}
+ for binary in variants.values():
+ if not binary.is_file():
+ parser.error(f'missing binary: {binary}')
+ args.scratch_parent.mkdir(parents=True, exist_ok=True)
+ with tempfile.TemporaryDirectory(prefix='pure-storage-timing-',
+ dir=args.scratch_parent.resolve()) as name:
+ root = Path(name)
+ (root / 'initial').write_bytes(INITIAL)
+ (root / 'replacement').write_bytes(b'Z')
+ filesystem = subprocess.run(['findmnt', '-T', str(root), '-no', 'FSTYPE,SOURCE'],
+ capture_output=True, text=True, check=True).stdout.strip()
+ samples = {name: [] for name in variants}
+ names = list(variants)
+ for trial in range(args.trials):
+ for name in names[trial % 3:] + names[:trial % 3]:
+ sample = one(variants[name], root / f'{name}-{trial}',
+ root / 'initial', root / 'replacement')
+ samples[name].append({'trial': trial, **sample})
+ result = {'profile': 'native-cli-durable-one-mib-document-last-byte-edit',
+ 'platform': platform.platform(), 'filesystem': filesystem,
+ 'trials_per_variant': args.trials,
+ 'final_body_sha256': hashlib.sha256(EXPECTED).hexdigest(),
+ 'scope': ('Each sample starts a new CLI process; create and edit each '
+ 'include program parsing, store initialization, file and '
+ 'directory synchronization. Show verifies the final revision '
+ 'but is outside the timed operations. The host page cache, '
+ 'filesystem and device remain uncontrolled.'),
+ 'variants': {name: {'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
+ 'median_create_ms': round(statistics.median(
+ row['create_ms'] for row in samples[name]), 3),
+ 'median_edit_ms': round(statistics.median(
+ row['edit_ms'] for row in samples[name]), 3),
+ 'samples': samples[name]}
+ for name, binary in variants.items()}}
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.write_text(json.dumps(result, indent=2) + '\n')
+ print(json.dumps({name: {key: value for key, value in entry.items()
+ if key.startswith('median_')}
+ for name, entry in result['variants'].items()}, indent=2))
+
+
+if __name__ == '__main__':
+ main()