sharing_probes.py (7035B)
1 #!/usr/bin/env python3 2 """Bounded Haskell-only sharing probes using the shipped stable compiler. 3 4 Fresh processes; separate full-result forcing from loading and rendering. These 5 are synthetic characterization probes, not performance gates or reduction counts. 6 """ 7 import argparse 8 import hashlib 9 import json 10 from pathlib import Path 11 import re 12 import statistics 13 import struct 14 import subprocess 15 import time 16 17 from bundle_dag import Intern, read 18 from stable_bundle import assembled 19 20 WORK = '''work [] = [] 21 work [head | tail] = t (t head) (work tail) 22 work _ = t 23 ''' 24 COMPARE = '''eq t t = t t 25 eq (t a) (t b) = eq a b 26 eq (t a b) (t c d) = case (eq a c) of 27 (t _) = eq b d 28 _ = t 29 eq _ _ = t 30 main input = eq input input 31 ''' 32 33 34 def natural(n): 35 return b'\0' if not n else b'\2' + (b'\1\0' if n % 2 else b'\0') + natural(n // 2) 36 37 38 def main(): 39 parser = argparse.ArgumentParser(description=__doc__) 40 parser.add_argument('tricu', type=Path) 41 parser.add_argument('--directory', required=True, type=Path) 42 parser.add_argument('--repeats', type=int, default=3) 43 args = parser.parse_args() 44 if not 1 <= args.repeats <= 5: 45 parser.error('--repeats must be within 1..5') 46 host, root = args.tricu.resolve(), args.directory.resolve() 47 root.mkdir(parents=True, exist_ok=False) 48 store = root / 'store' 49 started = time.monotonic() 50 report = dict(host=str(host), scope='fresh-process-full-result-forcing-v1', 51 commands=[], artifacts={}, samples=[], summaries=[]) 52 53 def save(): 54 (root / 'report.json').write_text(json.dumps(report, indent=2) + '\n') 55 56 def run(options, seconds=20, allow_failure=False): 57 remaining = 180 - (time.monotonic() - started) 58 if remaining <= 0: 59 raise TimeoutError('180-second whole-probe budget exhausted') 60 command = [str(host), *map(str, options), '+RTS', '-N1', '-T', '-M512m', '-RTS'] 61 begin = time.monotonic() 62 try: 63 reply = subprocess.run(command, capture_output=True, text=True, 64 timeout=min(seconds, remaining)) 65 except subprocess.TimeoutExpired: 66 report['commands'].append(dict(command=command, outcome='timeout')) 67 save() 68 if allow_failure: 69 return None 70 raise 71 report['commands'].append(dict(command=command, returncode=reply.returncode, 72 seconds=time.monotonic() - begin, 73 stdout=reply.stdout, stderr=reply.stderr)) 74 save() 75 if reply.returncode and not allow_failure: 76 raise RuntimeError(reply.stderr) 77 return reply 78 79 def imported(path): 80 reply = run(['arboricx', 'import', '--file', path, '--store', store]) 81 return re.search(r'Created module alias .* -> ([0-9a-f]{64})', reply.stdout)[1] 82 83 def compile_program(name, body): 84 source, bundle = root / (name + '.tri'), root / (name + '.arboricx') 85 source.write_text('!export main\n' + body + '\n') 86 run(['arboricx', 'compile', '--file', source, '--store', store, '--output', bundle]) 87 pin = imported(bundle) 88 report['artifacts'][name] = dict(manifest=pin, 89 source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(), 90 bundle_sha256=hashlib.sha256(bundle.read_bytes()).hexdigest()) 91 save() 92 return pin 93 94 def invocation(pin, arguments, result=False): 95 return ['arboricx', 'run', '--module', pin, '--name', 'main', '--store', store, 96 '--format', 'number' if result else 'ternary', 97 *(['--result'] if result else []), *arguments] 98 99 def measure(label, size, command, expected): 100 rows = [] 101 for repeat in range(args.repeats): 102 reply = run([*command, '--profile', '--profile-full-result']) 103 assert reply.stdout == expected, (label, size, reply.stdout) 104 samples = {v['profile']: v for line in reply.stderr.splitlines() 105 if line.startswith('{') for v in [json.loads(line)]} 106 before, after = samples['force-begin-1'], samples['force-end-1'] 107 row = dict(label=label, size=size, repeat=repeat, 108 force_ns=after['wall_ns'] - before['wall_ns'], 109 sampled_allocated_bytes=after['allocated_bytes'] - before['allocated_bytes']) 110 rows.append(row) 111 report['samples'].append(row) 112 summary = dict(label=label, size=size, 113 median_force_ns=statistics.median(r['force_ns'] for r in rows), 114 median_sampled_allocated_bytes=statistics.median(r['sampled_allocated_bytes'] for r in rows)) 115 report['summaries'].append(summary) 116 save() 117 print(json.dumps(summary), flush=True) 118 119 pins = {} 120 for name, body in [('bound', 'let shared = work input in t shared shared'), 121 ('repeated', 't (work input) (work input)')]: 122 pins[name] = compile_program(name, WORK + 'main input = t (t t) (t 23 (' + body + '))') 123 # Independent complete-output control, not merely comparison between variants. 124 xs = b''.join(b'\2\1' + natural(97) for _ in range(4)) + b'\0' 125 expected = b'\2\1\0\2' + natural(23) + b'\2' + xs + xs 126 for pin in pins.values(): 127 assert run(invocation(pin, ['--arg', 'aaaa'])).stdout == ''.join(str(v) for v in expected) 128 for size in [32, 256, 2048]: 129 for name, pin in pins.items(): 130 measure(name, size, invocation(pin, ['--arg', 'a' * size], result=True), 'ok: 23') 131 132 comparator = compile_program('compare', COMPARE) 133 template = read(root / 'compare.arboricx') 134 for depth in [4, 8, 12, 16]: 135 intern = Intern() 136 index = intern.node((0,)) 137 for _ in range(depth): 138 index = intern.node((2, index, index)) 139 manifest = bytearray(template[0]) 140 for offsets in template[4].values(): 141 for offset in offsets: 142 struct.pack_into('>I', manifest, offset, index) 143 bundle = root / f'dag-{depth}.arboricx' 144 bundle.write_bytes(assembled(manifest, intern.encoded())) 145 argument = imported(bundle) 146 measure('structural-self-comparison', depth, 147 invocation(comparator, ['--arg-export', argument + ':main']), '10') 148 149 # Previously --profile forced the divergent remainder. Only the explicit 150 # full-result mode should now fail; a bounded failure is not a divergence proof. 151 partial = compile_program('partial', 152 'main input = t (t t) (t 23 ((x : x x) (x : x x)))') 153 command = invocation(partial, ['--arg', 'x'], result=True) 154 for flags in [[], ['--profile']]: 155 assert run([*command, *flags]).stdout == 'ok: 23' 156 failed = run([*command, '--profile', '--profile-full-result'], seconds=3, allow_failure=True) 157 assert failed is None or failed.returncode != 0, 'full-result observation unexpectedly completed' 158 report['completed'] = True 159 save() 160 161 162 if __name__ == '__main__': 163 main()