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()