diff options
| author | eunhhu <[email protected]> | 2026-10-19 02:48:58 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-19 02:48:58 +0100 |
| commit | 7d41626b1c8f343b5b59a35ea3bce6570b996213 (patch) | |
| tree | 3f006bc4fdaeeb63be6a27996a86d464689caec2 /research/experiments | |
| parent | 46935dc0cc4711ef58fcc5129020f75c5e4e098e (diff) | |
| download | pure-7d41626b1c8f343b5b59a35ea3bce6570b996213.tar.gz pure-7d41626b1c8f343b5b59a35ea3bce6570b996213.zip | |
140's 1, DNS messages (RFC 1035), written in aup with no new opcode (aup's examples/dns.aup: a message read by a handler into its header and records, names uncompressed in their wire form, those in NS, CNAME, PTR, MX, SOA and SRV data too, each pointer pointing before the last; written back by the view wire, the owners compressed): 400 messages dnspython made (148,582 bytes, 3,466 records: A, AAAA, NS, CNAME, PTR, MX, SOA, TXT, SRV, EDNS, shared suffixes, 63-byte labels, bytes above 0x7f, 245-byte names) read on the three paths as dnspython reads them (each section's records as a set) and written back to bytes dnspython reads as the same message, the paths' outputs the same bytes; 300 damaged messages as dns_message.py (the Python reference) reads them, 185 refused by both and 115 the same records and bytes; module_specs in make check: 200 of those messages (tests/specs/dns, no dnspython at check time) read on the three paths, written back and read again to the same state and bytes, 23 cut ones failing; dnspython shuffles a set's records unless want_shuffle is off, and merges a set's differing TTLs to the least; bars: written, two workarounds both the core's (bytes have no ==: TEXT_COMPARE takes text alone; a view calls no block fn: CALL is in handlers and functions alone), 115 lines against Python's 72, 244 ns a message byte read and written against the same algorithm in C 3.65 (67x), glibc's resolver reading alone 4.4, pure Python 131 (Pure 1.9x slower), 218 compiled; 63.6 instructions a byte, reading 42%, writing 58%; CHECKPOINT's direction row
Diffstat (limited to 'research/experiments')
| -rw-r--r-- | research/experiments/044-specs/2026-10-19-dns.jsonl | 5 | ||||
| -rw-r--r-- | research/experiments/044-specs/dns_check.py | 166 | ||||
| -rw-r--r-- | research/experiments/044-specs/dns_message.py | 101 | ||||
| -rw-r--r-- | research/experiments/044-specs/dns_time.c | 255 |
4 files changed, 527 insertions, 0 deletions
diff --git a/research/experiments/044-specs/2026-10-19-dns.jsonl b/research/experiments/044-specs/2026-10-19-dns.jsonl new file mode 100644 index 0000000..05bd91d --- /dev/null +++ b/research/experiments/044-specs/2026-10-19-dns.jsonl @@ -0,0 +1,5 @@ +{"messages": 400, "bytes": 148582, "records": 3466, "right": {"batches": 400, "rounds": 400, "all": 400}, "paths_same": true, "damaged": 300, "damaged_refused": 185, "damaged_agreed": 300, "aup_lines": 115, "aup_brace_lines": 21, "python_lines": 72} +{"messages":400,"bytes":148582,"jit":"0","batch":"","c_ns_per_byte":3.646,"resolver_read_ns_per_byte":4.401,"pure_ns_per_byte":244.424,"to_c":67.0,"to_resolver_read":55.5,"instructions":9444428,"instructions_per_byte":63.6} +{"messages":400,"bytes":148582,"jit":"0","batch":"0","c_ns_per_byte":3.655,"resolver_read_ns_per_byte":4.404,"pure_ns_per_byte":243.692,"to_c":66.7,"to_resolver_read":55.3,"instructions":9444428,"instructions_per_byte":63.6} +{"messages":400,"bytes":148582,"jit":"all","batch":"","c_ns_per_byte":3.656,"resolver_read_ns_per_byte":4.401,"pure_ns_per_byte":217.982,"to_c":59.6,"to_resolver_read":49.5,"instructions":9444428,"instructions_per_byte":63.6} +{"messages":400,"bytes":148582,"python_ns_per_byte":130.9} diff --git a/research/experiments/044-specs/dns_check.py b/research/experiments/044-specs/dns_check.py new file mode 100644 index 0000000..d492fda --- /dev/null +++ b/research/experiments/044-specs/dns_check.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Research 140: DNS messages (RFC 1035) written in aup (aup's examples/dns.aup). + +Builds it with aup (AUP, default the sibling checkout) and reads messages dnspython made (queries and responses: A, +AAAA, NS, CNAME, PTR, MX, SOA, TXT and SRV records, EDNS, names sharing suffixes so compression points back, labels of +63 bytes and bytes above 0x7f, names near 255 bytes) on Pure's three paths (batches, rounds, all compiled): each +message's id, flags and records (names uncompressed) as dnspython reads them, and the message the view wire writes back +read by dnspython as the same message. Then damaged messages (cut short, a bit flipped) against dns_message.py, which +reads them the same way: the same records and bytes when it takes one, a failed event when it refuses it. Then counts +lines (code lines: neither blank nor a comment; brace lines: a closing brace alone) of dns.aup and of dns_message.py. +One JSON line. Given a path, it also writes the messages there for dns_time.c.""" +import json, os, random, subprocess, sys, tempfile + +import dns.flags, dns.message, dns.rrset + +here = os.path.dirname(os.path.abspath(__file__)) +pure = os.path.normpath(os.path.join(here, '../../..')) +aup = os.environ.get('AUP', os.path.join(pure, '..', 'aup')) +sys.path.insert(0, here) +import dns_message # noqa: E402 + + +def lines(path, stop=None): + code = braces = 0 + for line in open(path): + if stop and line.startswith(stop): break + text = line.strip() + if not text or text.startswith('#'): continue + code += 1 + braces += text == '}' + return code, braces + + +def messages(chooser, count): + def label(): + pick = chooser.random() + if pick < 0.05: + return ''.join(chooser.choices('abcdefghij', k=63)) + if pick < 0.1: + return ''.join(f'\\{chooser.randrange(128, 256):03d}' for _ in range(chooser.randrange(1, 4))) + return ''.join(chooser.choices('abcdefghijklmnopqrstuvwxyz0123456789-', k=chooser.randrange(1, 12))) + + zones = [f'{label()}.{chooser.choice(["com", "net", "org", "kr"])}.' for _ in range(6)] + + def name(): + pick = chooser.random() + if pick < 0.03: # 245 bytes + return '.'.join(''.join(chooser.choices('xyz', k=60)) for _ in range(4)) + '.' + if pick < 0.06: + return '.' + return '.'.join([label() for _ in range(chooser.randrange(0, 3))] + [chooser.choice(zones)]) + + def rdata(kind): + return {'A': lambda: '.'.join(str(chooser.randrange(256)) for _ in range(4)), + 'AAAA': lambda: ':'.join(f'{chooser.randrange(65536):x}' for _ in range(8)), + 'NS': name, 'CNAME': name, 'PTR': name, + 'MX': lambda: f'{chooser.randrange(65536)} {name()}', + 'SOA': lambda: f'{name()} {name()} ' + ' '.join(str(chooser.randrange(2 ** 32)) for _ in range(5)), + 'TXT': lambda: ' '.join('"' + ''.join(chooser.choices('abc xyz', k=chooser.randrange(0, 40))) + '"' + for _ in range(chooser.randrange(1, 4))), + 'SRV': lambda: f'{chooser.randrange(65536)} {chooser.randrange(65536)} {chooser.randrange(65536)} {name()}'}[kind]() + + kinds = ['A', 'AAAA', 'NS', 'CNAME', 'PTR', 'MX', 'SOA', 'TXT', 'SRV'] + out = [] + for _ in range(count): + q = dns.message.make_query(name(), chooser.choice(kinds), use_edns=0 if chooser.random() < 0.4 else None, + payload=chooser.choice([512, 1232, 4096]), id=chooser.randrange(65536)) + if chooser.random() < 0.2: + out.append(q) + continue + r = dns.message.make_response(q) + r.flags |= chooser.choice([0, dns.flags.AA, dns.flags.TC | dns.flags.AA, dns.flags.RA]) + r.set_rcode(chooser.choice([0, 0, 0, 2, 3, 5])) + ttls = {} # an RRset's records share a TTL (RFC 2181), so one name and type keep theirs + for section in (r.answer, r.authority, r.additional): + for _ in range(chooser.choice([0, 1, 1, 2, 4])): + kind, owner = chooser.choice(kinds), name() + ttl = ttls.setdefault((owner, kind), chooser.randrange(2 ** 31)) + section.append(dns.rrset.from_text(owner, ttl, 'IN', kind, *[rdata(kind) for _ in range(chooser.randrange(1, 4))])) + out.append(r) + return out + + +def seen(m): + """A message's id, flags and records (section, name, type, class, ttl, data) as dnspython reads it, each section's + records in order (dnspython gathers a section's records of one name and type into one set, so its order is not the + message's).""" + records = [(0, q.name.to_wire(), q.rdtype, q.rdclass, 0, b'') for q in m.question] + for section, rrsets in ((1, m.answer), (2, m.authority), (3, m.additional)): + for rrset in rrsets: + records += [(section, rrset.name.to_wire(), rrset.rdtype, rrset.rdclass, rrset.ttl, rd.to_wire()) for rd in rrset] + if m.opt: + records += [(3, b'\0', 41, m.opt.rdclass, m.opt.ttl, rd.to_wire()) for rd in m.opt] + return m.id, m.flags, sorted(records) + + +def damaged(chooser, wires): + out = [] + for wire in chooser.sample(wires, 150): + cut = wire[:chooser.randrange(len(wire))] + flipped = bytearray(wire) + flipped[chooser.randrange(len(wire))] ^= 1 << chooser.randrange(8) + for message in (cut, bytes(flipped)): + try: + id, flags, records = dns_message.read(message) + out.append((message, (id, flags, records, dns_message.write(id, flags, records)))) + except (ValueError, IndexError): + out.append((message, None)) + return out + + +def run(module, wires, env, work): + given = [] + for n, wire in enumerate(wires): + path = os.path.join(work, f'{n}.dns') + with open(path, 'wb') as f: + f.write(wire) + given.append(f'message read @{path}\nshow\n') + out = subprocess.run([os.path.join(pure, 'build', 'pure-run'), module], input=''.join(given), capture_output=True, + text=True, env=dict(os.environ, **env), timeout=1800).stdout.splitlines() + got = [] + for line in out[1::2]: # each message's show + state = json.loads(line) + s = state['states'] + records = [(r['section'], bytes.fromhex(r['name']), r['type'], r['class'], r['ttl'], bytes.fromhex(r['data'])) + for r in s['records']] + got.append((s['id'], s['flags'], records, bytes.fromhex(state['views']['wire']))) + failed = [json.loads(line)['fault'] != 'none' for line in out[0::2]] + return out, got, failed + + +chooser = random.Random(140) +made = messages(chooser, 400) +wires = [m.to_wire(max_size=65535, want_shuffle=False) for m in made] # dnspython shuffles a set's records +if len(sys.argv) > 1: # the messages for dns_time.c: each a u16 big-endian length, then its bytes (as over TCP) + with open(sys.argv[1], 'wb') as f: + f.write(b''.join(len(w).to_bytes(2, 'big') + w for w in wires)) +broken = damaged(chooser, wires) +paths = {} +with tempfile.TemporaryDirectory() as work: + module = os.path.join(work, 'dns.pmod') + subprocess.run([os.path.join(aup, 'build', 'aup'), 'build', os.path.join(aup, 'examples', 'dns.aup'), '-o', module], + check=True) + outputs = {} + for name, env in (('batches', {'PURE_JIT': '0'}), ('rounds', {'PURE_JIT': '0', 'PURE_BATCH': '0'}), + ('all', {'PURE_JIT': 'all'})): + raw, got, failed = run(module, wires, env, work) + right = 0 + for wire, g, f in zip(wires, got, failed): + read = dns.message.from_wire(wire) + right += not f and (g[0], g[1], sorted(g[2])) == seen(read) and dns.message.from_wire(g[3]) == read and \ + dns_message.read(wire)[2] == g[2] + paths[name] = right if len(got) == len(wires) else 0 + outputs[name] = raw + same_paths = outputs['rounds'] == outputs['batches'] and outputs['all'] == outputs['batches'] + raw, got, failed = run(module, [m for m, _ in broken], {}, work) + agreed = sum((want is None and f) or (want is not None and not f and g == want) + for (_, want), g, f in zip(broken, got, failed)) if len(got) == len(broken) else 0 + refused = sum(want is None for _, want in broken) +aup_code, aup_braces = lines(os.path.join(aup, 'examples', 'dns.aup')) +python_code, _ = lines(os.path.join(here, 'dns_message.py'), stop='if __name__') +ok = all(n == len(wires) for n in paths.values()) and same_paths and agreed == len(broken) +print(json.dumps({'messages': len(wires), 'bytes': sum(map(len, wires)), 'records': sum(len(dns_message.read(w)[2]) for w in wires), + 'right': paths, 'paths_same': same_paths, 'damaged': len(broken), 'damaged_refused': refused, + 'damaged_agreed': agreed, 'aup_lines': aup_code, 'aup_brace_lines': aup_braces, 'python_lines': python_code})) +sys.exit(0 if ok else 1) diff --git a/research/experiments/044-specs/dns_message.py b/research/experiments/044-specs/dns_message.py new file mode 100644 index 0000000..cf9ab87 --- /dev/null +++ b/research/experiments/044-specs/dns_message.py @@ -0,0 +1,101 @@ +# DNS messages (RFC 1035) in plain Python: the reference 140 counts aup's lines against. A message's bytes read into its +# header and records, names in their wire form uncompressed (the owner's and those inside NS, CNAME, PTR, MX, SOA and +# SRV data), and written back with the owners compressed. Each pointer points before the last, so they end. +NAMED = {2: 0, 5: 0, 12: 0, 15: 2, 33: 6} # NS, CNAME, PTR, MX, SRV: the bytes before the name + + +def name(d, i): + """The name at i uncompressed, and where it ends in place.""" + out, j, end, below = b'', i, None, i + while d[j]: + if d[j] >= 0xc0: + to = (d[j] & 0x3f) << 8 | d[j + 1] + if to >= below: + raise ValueError('a pointer forward') + end, below, j = end or j + 2, to, to + elif d[j] < 0x40: + out += d[j:j + d[j] + 1] + j += d[j] + 1 + else: + raise ValueError('a label type') + if len(out) >= 255: + raise ValueError('a name too long') + return out + b'\0', end or j + 1 + + +def data(d, i, size, type): + """A record's data, the names in it uncompressed.""" + if type in NAMED: + n, end = name(d, i + NAMED[type]) + if end != i + size: + raise ValueError('data length') + return d[i:i + NAMED[type]] + n + if type == 6: + m, after = name(d, i) + r, end = name(d, after) + if end + 20 != i + size: + raise ValueError('data length') + return m + r + d[end:end + 20] + return d[i:i + size] + + +def read(d): + """The id, the flags and the records (section, name, type, class, ttl, data) of a message.""" + if len(d) < 12: + raise ValueError('a header is 12 bytes') + word = lambda i: d[i] << 8 | d[i + 1] + records, at = [], 12 + for section in range(4): + for _ in range(word(4 + 2 * section)): + owner, at = name(d, at) + type, klass = word(at), word(at + 2) + at += 4 + if section == 0: + records.append((0, owner, type, klass, 0, b'')) + continue + ttl, size = word(at) << 16 | word(at + 2), word(at + 4) + if at + 6 + size > len(d): + raise ValueError('data length') + records.append((section, owner, type, klass, ttl, data(d, at + 6, size, type))) + at += 6 + size + if at != len(d): + raise ValueError('bytes after the records') + return word(0), word(2), records + + +def write(id, flags, records): + """The message's bytes, each owner from its first label whose rest was written before, as a pointer to it.""" + out = bytearray(id.to_bytes(2, 'big') + flags.to_bytes(2, 'big')) + for section in range(4): + out += sum(r[0] == section for r in records).to_bytes(2, 'big') + seen = {} + for section, owner, type, klass, ttl, rdata in records: + k = 0 + while owner[k] and owner[k:] not in seen: + if len(out) < 0x4000: + seen[owner[k:]] = len(out) + out += owner[k:k + owner[k] + 1] + k += owner[k] + 1 + out += (0xc000 | seen[owner[k:]]).to_bytes(2, 'big') if owner[k] else b'\0' + out += type.to_bytes(2, 'big') + klass.to_bytes(2, 'big') + if section: + out += ttl.to_bytes(4, 'big') + len(rdata).to_bytes(2, 'big') + rdata + return bytes(out) + + +if __name__ == '__main__': + import sys, time + with open(sys.argv[1], 'rb') as f: # dns_check.py's messages: each a u16 big-endian length, then its bytes + given = f.read() + messages, at = [], 0 + while at < len(given): + messages.append(given[at + 2:at + 2 + int.from_bytes(given[at:at + 2], 'big')]) + at += 2 + len(messages[-1]) + size = sum(map(len, messages)) + runs = [] + for _ in range(7): + start = time.perf_counter() + for m in messages: + write(*read(m)) + runs.append((time.perf_counter() - start) * 1e9 / size) + print('{"messages":%d,"bytes":%d,"python_ns_per_byte":%.1f}' % (len(messages), size, sorted(runs)[3])) diff --git a/research/experiments/044-specs/dns_time.c b/research/experiments/044-specs/dns_time.c new file mode 100644 index 0000000..b8c6563 --- /dev/null +++ b/research/experiments/044-specs/dns_time.c @@ -0,0 +1,255 @@ +/* Research 140: DNS messages (RFC 1035) in aup (aup's examples/dns.aup, run by Pure) against the same algorithm in C + * (each message read into its records, names uncompressed, then written back with the owners compressed) and against + * glibc's resolver reading them (ns_initparse, ns_parserr and ns_name_unpack for the names in data: reading only, the + * world's C has no writer of whole messages), on the same messages. Pure's and C's written bytes must be the same, and + * the resolver must take every message; then each is timed (the median of seven runs of rounds passes). + * + * cc -O2 -std=c17 -I../../../core dns_time.c ../../../build/libpure.a -lresolv -lm -pthread -o dns_time + * ./dns_time dns.pmod MESSAGES ROUNDS (MESSAGES: each a u16 big-endian length, then its bytes, as over TCP) + * + * One JSON line: the messages and their bytes, the nanoseconds per message byte of each, Pure's ratios and the + * instructions Pure counted for one pass (each event's and each view's). */ +#define _DEFAULT_SOURCE +#include "module.h" +#include <arpa/nameser.h> +#include <resolv.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +/* ---- the same algorithm in C */ +typedef struct { + int section, type, class; + unsigned long ttl; + unsigned char name[256], data[1024]; + size_t name_size, data_size; +} Record; + +static int word(const unsigned char *d, size_t i) { return d[i] << 8 | d[i + 1]; } + +/* The name at i uncompressed into out; where it ends in place, 0 if it is none. */ +static size_t name(const unsigned char *d, size_t size, size_t i, unsigned char *out, size_t *used) { + size_t j = i, end = 0, below = i, n = 0; + for (;;) { + if (j >= size) return 0; + if (!d[j]) break; + if (d[j] >= 0xc0) { + if (j + 1 >= size) return 0; + size_t to = (size_t)((d[j] & 0x3f) << 8 | d[j + 1]); + if (to >= below) return 0; + if (!end) end = j + 2; + below = j = to; + } else { + if (d[j] >= 0x40 || j + d[j] + 1 > size || n + d[j] + 1 >= 255) return 0; + memcpy(out + n, d + j, d[j] + 1u); + n += d[j] + 1u; + j += d[j] + 1u; + } + } + out[n++] = 0; + *used = n; + return end ? end : j + 1; +} + +static int data(const unsigned char *d, size_t size, size_t i, size_t length, int type, Record *r) { + int skip = type == 15 ? 2 : type == 33 ? 6 : 0; + size_t used, end; + if (type == 2 || type == 5 || type == 12 || type == 15 || type == 33) { + memcpy(r->data, d + i, (size_t)skip); + if ((end = name(d, size, i + (size_t)skip, r->data + skip, &used)) != i + length) return -1; + r->data_size = (size_t)skip + used; + } else if (type == 6) { + size_t a, b; + if (!(end = name(d, size, i, r->data, &a)) || (end = name(d, size, end, r->data + a, &b)) + 20 != i + length) return -1; + memcpy(r->data + a + b, d + end, 20); + r->data_size = a + b + 20; + } else { + if (length > sizeof(r->data)) return -1; + memcpy(r->data, d + i, length); + r->data_size = length; + } + return 0; +} + +/* The records of a message (at most room); their count, -1 if it is none. */ +static int read_message(const unsigned char *d, size_t size, Record *records, int room, int *id, int *flags) { + if (size < 12) return -1; + int n = 0; + size_t at = 12; + for (int section = 0; section < 4; ++section) + for (int k = word(d, 4 + 2 * (size_t)section); k > 0; --k) { + if (n == room) return -1; + Record *r = &records[n++]; + size_t next = name(d, size, at, r->name, &r->name_size); + if (!next || next + 4 > size) return -1; + r->section = section, r->type = word(d, next), r->class = word(d, next + 2), r->ttl = 0, r->data_size = 0; + at = next + 4; + if (section) { + if (at + 6 > size) return -1; + size_t length = (size_t)word(d, at + 4); + if (at + 6 + length > size) return -1; + r->ttl = (unsigned long)word(d, at) << 16 | (unsigned long)word(d, at + 2); + if (data(d, size, at + 6, length, r->type, r)) return -1; + at += 6 + length; + } + } + if (at != size) return -1; + *id = word(d, 0), *flags = word(d, 2); + return n; +} + +static void two(unsigned char *out, size_t *n, unsigned v) { out[(*n)++] = (unsigned char)(v >> 8), out[(*n)++] = (unsigned char)v; } + +/* The message's bytes, each owner from its first label whose rest was written before, as a pointer to it. */ +static size_t write_message(const Record *records, int count, int id, int flags, unsigned char *out) { + static struct { const unsigned char *name; size_t size, at; } seen[4096]; + size_t n = 0, kept = 0; + two(out, &n, (unsigned)id), two(out, &n, (unsigned)flags); + for (int section = 0; section < 4; ++section) { + unsigned c = 0; + for (int i = 0; i < count; ++i) c += records[i].section == section; + two(out, &n, c); + } + for (int i = 0; i < count; ++i) { + const Record *r = &records[i]; + size_t k = 0; + bool done = false; + while (!done && r->name[k]) { + for (size_t s = 0; s < kept && !done; ++s) + if (seen[s].size == r->name_size - k && !memcmp(seen[s].name, r->name + k, seen[s].size)) { + two(out, &n, 0xc000u | (unsigned)seen[s].at); + done = true; + } + if (!done) { + if (n < 0x4000 && kept < 4096) seen[kept].name = r->name + k, seen[kept].size = r->name_size - k, seen[kept++].at = n; + memcpy(out + n, r->name + k, r->name[k] + 1u); + n += r->name[k] + 1u; + k += r->name[k] + 1u; + } + } + if (!done) out[n++] = 0; + two(out, &n, (unsigned)r->type), two(out, &n, (unsigned)r->class); + if (r->section) { + two(out, &n, (unsigned)(r->ttl >> 16)), two(out, &n, (unsigned)r->ttl), two(out, &n, (unsigned)r->data_size); + memcpy(out + n, r->data, r->data_size); + n += r->data_size; + } + } + return n; +} + +/* ---- glibc's resolver reading a message: every record, and the names in NS, CNAME, PTR, MX, SOA and SRV data */ +static int resolver_read(const unsigned char *d, size_t size) { + ns_msg msg; + if (ns_initparse(d, (int)size, &msg)) return -1; + int n = 0; + for (int section = 0; section < 4; ++section) + for (int k = 0; k < ns_msg_count(msg, (ns_sect)section); ++k) { + ns_rr rr; + if (ns_parserr(&msg, (ns_sect)section, k, &rr)) return -1; + ++n; + int type = ns_rr_type(rr), skip = type == 15 ? 2 : type == 33 ? 6 : 0; + unsigned char name[NS_MAXCDNAME]; + if (section && (type == 2 || type == 5 || type == 12 || type == 15 || type == 33 || type == 6)) { + const unsigned char *at = ns_rr_rdata(rr) + skip; + int used = ns_name_unpack(d, d + size, at, name, sizeof(name)); + if (used < 0 || (type == 6 && ns_name_unpack(d, d + size, at + used, name, sizeof(name)) < 0)) return -1; + } + } + return n; +} + +static double now_ns(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (double)t.tv_sec * 1e9 + (double)t.tv_nsec; +} + +static int by_value(const void *a, const void *b) { + double x = *(const double *)a, y = *(const double *)b; + return (x > y) - (x < y); +} + +typedef struct { unsigned char *bytes; size_t used, room; } Gathered; +static bool gather(void *context, PureSpan span) { + Gathered *g = context; + if (g->used + span.size > g->room) return false; + memcpy(g->bytes + g->used, span.data, span.size); + g->used += span.size; + return true; +} + +int main(int argc, char **argv) { + if (argc != 4) { fprintf(stderr, "usage: %s MODULE MESSAGES ROUNDS\n", argv[0]); return 2; } + FILE *f = fopen(argv[2], "rb"); + if (!f) { fprintf(stderr, "cannot read %s\n", argv[2]); return 1; } + static unsigned char all[1 << 24]; + size_t size = fread(all, 1, sizeof(all), f); + fclose(f); + int rounds = atoi(argv[3]), count = 0; + static size_t starts[65536], sizes[65536]; + size_t bytes = 0; + for (size_t at = 0; at + 2 <= size && count < 65536; at += 2 + sizes[count++]) + starts[count] = at + 2, sizes[count] = (size_t)word(all, at), bytes += sizes[count]; + + PureModule module = {0}; + char error[160]; + if (pure_module_read(argv[1], &module, error, sizeof(error)) != PURE_OK) { fprintf(stderr, "%s\n", error); return 1; } + PureApp app; + if (pure_app_start(&app, &module, (PureData){0}) != PURE_OK) return 1; + int input = pure_module_input(&module, "read"), view = pure_module_view(&module, "wire"); + static Record records[4096]; + static unsigned char written[1 << 17], viewed[1 << 17]; + uint64_t work = 0; /* what each event and each view evaluation used (app.work counts the unit running) */ + for (int m = 0; m < count; ++m) { + int id, flags, n = read_message(all + starts[m], sizes[m], records, 4096, &id, &flags); + if (n < 0 || resolver_read(all + starts[m], sizes[m]) != n) { fprintf(stderr, "message %d not read\n", m); return 1; } + size_t length = write_message(records, n, id, flags, written); + Gathered g = {viewed, 0, sizeof(viewed)}; + bool read = pure_app_message(&app, (uint32_t)input, (PureSpan){all + starts[m], sizes[m]}) == PURE_OK; + work += app.work; + if (!read || pure_app_view_text(&app, (uint32_t)view, gather, &g) != PURE_OK || g.used != length || + memcmp(viewed, written, length)) { + fprintf(stderr, "message %d: Pure's bytes differ\n", m); + return 1; + } + work += app.work; + } + + double c_runs[7], resolver_runs[7], pure_runs[7]; + volatile size_t sink = 0; + for (int r = 0; r < 7; ++r) { + double start = now_ns(); + for (int i = 0; i < rounds; ++i) + for (int m = 0; m < count; ++m) { + int id, flags, n = read_message(all + starts[m], sizes[m], records, 4096, &id, &flags); + sink += write_message(records, n, id, flags, written); + } + c_runs[r] = (now_ns() - start) / rounds; + start = now_ns(); + for (int i = 0; i < rounds; ++i) + for (int m = 0; m < count; ++m) sink += (size_t)resolver_read(all + starts[m], sizes[m]); + resolver_runs[r] = (now_ns() - start) / rounds; + start = now_ns(); + for (int i = 0; i < rounds; ++i) + for (int m = 0; m < count; ++m) { + Gathered g = {viewed, 0, sizeof(viewed)}; + if (pure_app_message(&app, (uint32_t)input, (PureSpan){all + starts[m], sizes[m]}) != PURE_OK || + pure_app_view_text(&app, (uint32_t)view, gather, &g) != PURE_OK) return 1; + sink += g.used; + } + pure_runs[r] = (now_ns() - start) / rounds; + } + qsort(c_runs, 7, sizeof(double), by_value); + qsort(resolver_runs, 7, sizeof(double), by_value); + qsort(pure_runs, 7, sizeof(double), by_value); + double n = (double)bytes; + const char *jit = getenv("PURE_JIT"), *batch = getenv("PURE_BATCH"); + printf("{\"messages\":%d,\"bytes\":%zu,\"jit\":\"%s\",\"batch\":\"%s\",\"c_ns_per_byte\":%.3f,\"resolver_read_ns_per_byte\":%.3f," + "\"pure_ns_per_byte\":%.3f,\"to_c\":%.1f,\"to_resolver_read\":%.1f,\"instructions\":%llu,\"instructions_per_byte\":%.1f}\n", + count, bytes, jit ? jit : "", batch ? batch : "", c_runs[3] / n, resolver_runs[3] / n, pure_runs[3] / n, + pure_runs[3] / c_runs[3], pure_runs[3] / resolver_runs[3], (unsigned long long)work, (double)work / n); + pure_app_stop(&app); + return 0; +} |
