tricu

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

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