tricu

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

stable_costs.py (4813B)


      1 #!/usr/bin/env python3
      2 """Small cold-invocation cost check, reusing stable_abstraction.py artifacts.
      3 
      4 python3 test/stable_costs.py result/bin/tricu /tmp/tricu-stable
      5 Each sample is a new process: no repeated pure-result sharing. GC-backed
      6 invocation deltas exclude loading, include complete result traversal and ending
      7 GC. This is a feasibility check, not a warm-runtime benchmark or compiler peak
      8 memory measurement. Exact output equality is checked outside the timed region.
      9 """
     10 import argparse
     11 import json
     12 from pathlib import Path
     13 import re
     14 import statistics
     15 import subprocess
     16 
     17 ROOT = Path(__file__).resolve().parent.parent
     18 
     19 
     20 def main():
     21     parser = argparse.ArgumentParser(description=__doc__)
     22     parser.add_argument('tricu', type=Path)
     23     parser.add_argument('directory', type=Path)
     24     args = parser.parse_args()
     25     parser.error("Retired implicit recovery preparation: this historical workflow has no verified native-support input route. Use current portable candidate tooling; do not reuse its old support/cache artifacts.")
     26     tricu = str(args.tricu.resolve())
     27     artifacts = args.directory.resolve()
     28     root = artifacts / 'costs'
     29     root.mkdir(exist_ok=True)
     30     store = root / 'store'
     31 
     32     def run(command):
     33         result = subprocess.run([tricu, *map(str, command)], cwd=ROOT,
     34                                 capture_output=True, timeout=120)
     35         assert result.returncode == 0, (command, result.stderr)
     36         return result
     37 
     38     # Same algorithm and library parsers; handwritten fixed point and thunks.
     39     reference = root / 'reference.tri'
     40     reference.write_text('!import "prelude" !Local\n!import "parser" P\n' +
     41         'repeat_ self input parser reversed = (P.parserMatchReply\n'
     42         '  (problem rest consumed committed : lazyBool\n'
     43         '    (_ : err problem rest)\n'
     44         '    (_ : ok (reverse reversed) input) (or? consumed committed))\n'
     45         '  (value rest consumed committed : lazyBool\n'
     46         '    (_ : self rest parser (pair value reversed))\n'
     47         '    (_ : err "non-consuming parser" input) consumed)\n'
     48         '  (parser input))\n'
     49         'main input = y repeat_ input (P.parserThen P.parserAny P.parserAny) []\n')
     50     reference_bundle = root / 'reference.arboricx'
     51     run(['arboricx', 'compile', '--file', reference, '--store', artifacts / 'store',
     52          '--name', 'main', '--output', reference_bundle])
     53     stable_bundle = artifacts / 'repeat-empty.arboricx'
     54     if not stable_bundle.exists():
     55         stable_bundle = artifacts / 'repeat-pairs.arboricx'
     56     pins = {}
     57     for label, bundle in [('reference', reference_bundle), ('stable', stable_bundle)]:
     58         reply = run(['arboricx', 'import', '--file', bundle, '--store', store]).stdout
     59         pins[label] = re.search(rb'Created module alias .* -> ([0-9a-f]{64})', reply)[1].decode()
     60 
     61     results = []
     62     for size in [512, 2048]:
     63         text = 'ab' * (size // 2)
     64         # Independently construct the expected Result tree using literal data.
     65         golden = root / 'golden.tri'
     66         golden.write_text('!import "prelude" !Local\n' +
     67                           'main = ok ' + json.dumps('b' * (size // 2)) + ' []\n')
     68         expected = run(['eval', golden, '--store', artifacts / 'store', '--format', 'tree']).stdout
     69         for sample in range(3):
     70             for label in (['reference', 'stable'] if sample % 2 == 0 else ['stable', 'reference']):
     71                 reply = run(['arboricx', 'run', '--store', store, '--module', pins[label],
     72                     '--name', 'main', '--arg', text, '--format', 'tree', '--profile', '+RTS', '-T', '-RTS'])
     73                 assert reply.stdout == expected, (label, size, 'wrong output')
     74                 profile = [json.loads(line) for line in reply.stderr.splitlines() if line.startswith(b'{')]
     75                 results.append(dict(label=label, input_bytes=size, sample=sample, profile=profile))
     76     (root / 'samples.json').write_text(json.dumps(results, indent=2) + '\n')
     77     for size in [512, 2048]:
     78         for label in pins:
     79             selected = [r['profile'] for r in results if r['input_bytes'] == size and r['label'] == label]
     80             # Keep labels explicit: fail rather than silently time the wrong region.
     81             intervals = []
     82             for profile in selected:
     83                 begin = next(p for p in profile if p['profile'] == 'invoke-begin-1')
     84                 end = next(p for p in profile if p['profile'] == 'invoke-end-1')
     85                 intervals.append((end['wall_ns'] - begin['wall_ns'],
     86                                   end['allocated_bytes'] - begin['allocated_bytes']))
     87             print(f'{size} bytes {label}: median {statistics.median(i[0] for i in intervals)/1e6:.2f} ms; '
     88                   f'{statistics.median(i[1] for i in intervals)/1e6:.2f} MB allocated')
     89 
     90 
     91 if __name__ == '__main__':
     92     main()