aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/005-storage-edit-latency/sequence.py
diff options
context:
space:
mode:
Diffstat (limited to 'research/experiments/005-storage-edit-latency/sequence.py')
-rw-r--r--research/experiments/005-storage-edit-latency/sequence.py85
1 files changed, 85 insertions, 0 deletions
diff --git a/research/experiments/005-storage-edit-latency/sequence.py b/research/experiments/005-storage-edit-latency/sequence.py
new file mode 100644
index 0000000..57ff835
--- /dev/null
+++ b/research/experiments/005-storage-edit-latency/sequence.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+"""Measure repeated durable edits and page-segment rebuilds on one real document."""
+import argparse
+import hashlib
+import json
+from pathlib import Path
+import platform
+import statistics
+import subprocess
+import tempfile
+
+from measure import INITIAL, PROGRAM, invoke
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--flat', type=Path, required=True)
+ parser.add_argument('--paged', type=Path, required=True)
+ parser.add_argument('--scratch-parent', type=Path, required=True)
+ parser.add_argument('--edits', type=int, default=70)
+ parser.add_argument('--output', type=Path, required=True)
+ args = parser.parse_args()
+ if args.edits < 66 or args.output.exists():
+ parser.error('at least 66 edits are needed to observe the current rebuild boundary; output must be new')
+ binaries = {'flat': args.flat.resolve(), 'paged': args.paged.resolve()}
+ args.scratch_parent.mkdir(parents=True, exist_ok=True)
+ with tempfile.TemporaryDirectory(prefix='pure-storage-sequence-',
+ dir=args.scratch_parent.resolve()) as name:
+ root = Path(name)
+ filesystem = subprocess.run(['findmnt', '-T', str(root), '-no', 'FSTYPE,SOURCE'],
+ capture_output=True, text=True, check=True).stdout.strip()
+ (root / 'initial').write_bytes(INITIAL)
+ patches = {'Y': root / 'Y', 'Z': root / 'Z'}
+ for label, file in patches.items():
+ file.write_bytes(label.encode())
+ bases = {variant: [binary, '--program', PROGRAM, '--store', root / variant]
+ for variant, binary in binaries.items()}
+ for base in bases.values():
+ result, _ = invoke([*base, 'edit', 'document', 0, 0, 0, root / 'initial'])
+ assert json.loads(result.stdout)['revision'] == '1'
+ samples = {variant: [] for variant in binaries}
+ for edit in range(1, args.edits + 1):
+ patch = 'Z' if edit % 2 else 'Y'
+ order = ('flat', 'paged') if edit % 2 else ('paged', 'flat')
+ for variant in order:
+ base = bases[variant]
+ result, elapsed = invoke([*base, 'edit', 'document', edit,
+ len(INITIAL) - 1, 1, patches[patch]])
+ assert json.loads(result.stdout)['revision'] == str(edit + 1)
+ files = [file for file in (root / variant).iterdir() if file.is_file()]
+ samples[variant].append({'edit': edit, 'elapsed_ms': round(elapsed, 3),
+ 'files': len(files),
+ 'allocated_bytes': sum(file.stat().st_blocks * 512
+ for file in files)})
+ expected = INITIAL[:-1] + patches[patch].read_bytes()
+ for base in bases.values():
+ shown, _ = invoke([*base, 'show', 'document'])
+ assert shown.stdout == expected
+ assert json.loads(shown.stderr)['revision'] == str(args.edits + 1)
+ result = {'profile': 'repeated-durable-one-mib-document-last-byte-edit',
+ 'platform': platform.platform(), 'filesystem': filesystem,
+ 'edits_per_variant': args.edits,
+ 'final_body_sha256': hashlib.sha256(expected).hexdigest(),
+ 'scope': ('Same actual source-loaded CLI and editor.pure, two persistent '
+ 'stores, alternating operation order. Timings include each '
+ 'edit process, parser, store init and fsync; creation and '
+ 'final show are not timed. Device state is uncontrolled.'),
+ 'variants': {variant: {
+ 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
+ 'median_edit_ms': round(statistics.median(row['elapsed_ms']
+ for row in samples[variant]), 3),
+ 'samples': samples[variant]}
+ for variant, binary in binaries.items()}}
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.write_text(json.dumps(result, indent=2) + '\n')
+ print(json.dumps({variant: {'median_edit_ms': row['median_edit_ms'],
+ 'max_edit_ms': max(sample['elapsed_ms']
+ for sample in row['samples']),
+ 'max_allocated_bytes': max(sample['allocated_bytes']
+ for sample in row['samples'])}
+ for variant, row in result['variants'].items()}, indent=2))
+
+
+if __name__ == '__main__':
+ main()