aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/005-storage-edit-latency/measure.py
blob: b390c92bc27ca86fa48ccfb5e12d35ca50ddb1c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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()