io_storage_cost.py (5350B)
1 #!/usr/bin/env python3 2 """Isolated storage hash/write cost guards; no compiler generations or bootstrap changes. 3 4 Optional --reference-storage compiles an explicitly selected historical source 5 with the same inputs and probe. Its results are measured without the candidate 6 allocation guard. Wall timings are observations, not portable acceptance limits. 7 """ 8 import argparse 9 import hashlib 10 import json 11 import statistics 12 from pathlib import Path 13 14 from http_headers import number, result 15 from source_order import Runner 16 from stable_bundle import byte_tree 17 18 ROOT = Path(__file__).resolve().parent.parent 19 SOURCE = '''!import io as IO 20 !import io_storage as S 21 !export hash 22 !export write 23 hash limit bytes = IO.io (S.hashBytes limit bytes) 24 write path bytes = IO.io <| IO.scope (owner : do IO.onOk 25 writer <- IO.openWriter owner path 26 IO.writeAll writer bytes) 27 ''' 28 29 30 def main(): 31 parser = argparse.ArgumentParser(description=__doc__) 32 parser.add_argument('tricu', type=Path) 33 parser.add_argument('--directory', type=Path, required=True) 34 parser.add_argument('--reference-storage', type=Path) 35 args = parser.parse_args() 36 root = args.directory.resolve() 37 root.mkdir(parents=True, exist_ok=False) 38 source = root/'source' 39 source.mkdir() 40 inputs = source/'inputs' 41 inputs.mkdir() 42 paths = dict(base=ROOT/'lib/base.tri', native_math=ROOT/'lib/math/native.tri', 43 io=ROOT/'lib/io.tri', 44 io_storage=args.reference_storage or ROOT/'lib/io/storage.tri') 45 workspace = ['tricu.workspace.v1'] 46 for name, path in paths.items(): 47 (inputs/(name+'.tri')).write_bytes(path.read_bytes()) 48 workspace.append(name+'\tinputs/'+name+'.tri') 49 (source/'tricu.workspace').write_text('\n'.join(workspace)+'\n') 50 (source/'probe.tri').write_text(SOURCE) 51 runner = Runner(args.tricu, root) 52 bundle = root/'probe.arboricx' 53 runner.run('compile', ['compile', '--file', source/'probe.tri', '--store', runner.store, 54 '--output', bundle]) 55 pin = runner.imported(bundle, 'import') 56 summaries = [] 57 for size in [1024, 4096, 65536]: 58 payload = bytes(range(256))*(size//256) 59 argument = root/f'bytes-{size}.tree' 60 argument.write_bytes(byte_tree(payload)) 61 expected = result(True, byte_tree(hashlib.sha256(payload).hexdigest().encode())) 62 for label, limit in [('exact', size), ('roomy', 67108864)]: 63 bound = root/f'limit-{size}-{label}.tree' 64 bound.write_bytes(number(limit)) 65 rows = [] 66 for repeat in range(3): 67 actual = runner.invoke(f'hash-{size}-{label}-{repeat}', pin, 'hash', 68 ['--io', '--arg-tree-file', bound, '--arg-tree-file', argument]) 69 assert actual == expected, (size, label, repeat) 70 rows.append(runner.records[-1]) 71 summary = dict(bytes=size, limit=limit, repeats=len(rows), 72 median_invocation_seconds=statistics.median(r['invoke_seconds'] for r in rows), 73 median_invocation_allocated_bytes=statistics.median(r['allocated_bytes'] for r in rows)) 74 # Includes result observation. Allow ample headroom over the measured 75 # fixed-step helper while rejecting the numeric-countdown/list-contract 76 # path (~406 MB for one 4-KiB chunk on the reference compiler). 77 if not args.reference_storage and size == 4096 and label == 'roomy': 78 assert summary['median_invocation_allocated_bytes'] < 160_000_000, summary 79 summaries.append(summary) 80 writes = [] 81 # Full-bound native acknowledgements exhaust the validated input chunk. 82 # The neighboring short chunk continues to exercise ordinary dropping. 83 for size in [4095,4096]: 84 payload=(bytes(range(256))*16)[:size] 85 argument=root/f'write-bytes-{size}.tree' 86 argument.write_bytes(byte_tree(payload)) 87 destination=root/f'write-{size}.bin' 88 path=root/f'write-path-{size}.tree' 89 path.write_bytes(byte_tree(str(destination).encode())) 90 rows=[] 91 for repeat in range(3): 92 actual=runner.invoke(f'write-{size}-{repeat}',pin,'write', 93 ['--io','--arg-tree-file',path,'--arg-tree-file',argument]) 94 assert actual == result(True,b'\0') 95 assert destination.read_bytes() == payload 96 rows.append(runner.records[-1]) 97 summary=dict(bytes=size,repeats=len(rows), 98 median_invocation_seconds=statistics.median(r['invoke_seconds'] for r in rows), 99 median_invocation_allocated_bytes=statistics.median(r['allocated_bytes'] for r in rows)) 100 if size == 4096: 101 assert summary['median_invocation_allocated_bytes'] < 32_000_000,summary 102 writes.append(summary) 103 report = dict(reference_storage=str(args.reference_storage) if args.reference_storage else None, 104 source_hashes={name: hashlib.sha256(path.read_bytes()).hexdigest() for name, path in paths.items()}, 105 bundle_sha256=hashlib.sha256(bundle.read_bytes()).hexdigest(), 106 measurements=summaries, write_measurements=writes, records=runner.records) 107 (root/'report.json').write_text(json.dumps(report, indent=2)+'\n') 108 print(json.dumps(dict(hashes=summaries,writes=writes), indent=2)) 109 110 111 if __name__ == '__main__': 112 main()