tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

io_v2_storage_cost.py (5368B)


      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_v2 as IO
     20 !import io_v2_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_v2=ROOT/'lib/io-v2.tri',
     44                  io_v2_storage=args.reference_storage or ROOT/'lib/io-v2/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()