aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/044-specs
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 02:48:58 +0100
committereunhhu <[email protected]>2026-10-19 02:48:58 +0100
commit7d41626b1c8f343b5b59a35ea3bce6570b996213 (patch)
tree3f006bc4fdaeeb63be6a27996a86d464689caec2 /research/experiments/044-specs
parent46935dc0cc4711ef58fcc5129020f75c5e4e098e (diff)
downloadpure-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/044-specs')
-rw-r--r--research/experiments/044-specs/2026-10-19-dns.jsonl5
-rw-r--r--research/experiments/044-specs/dns_check.py166
-rw-r--r--research/experiments/044-specs/dns_message.py101
-rw-r--r--research/experiments/044-specs/dns_time.c255
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;
+}