diff options
| author | eunhhu <[email protected]> | 2026-10-19 03:02:51 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-19 03:02:51 +0100 |
| commit | 5842b26e8353a38192afb1ebe4c08809da48dbf4 (patch) | |
| tree | a4bf3d5e8ad8194b37d1beb1fa3639a6fd12c279 /research | |
| parent | 85a35189c6588903ffc287837c9005ef3063e056 (diff) | |
| download | pure-5842b26e8353a38192afb1ebe4c08809da48dbf4.tar.gz pure-5842b26e8353a38192afb1ebe4c08809da48dbf4.zip | |
140's 9, HTTP/1.1 requests a packet at a time (RFC 9112), written in aup with no new opcode (aup's examples/http.aup: each packet an event, its bytes added to what the last left, every request they complete read, h11's grammar): 200 streams (132,623 bytes, 578 requests: methods, targets, HTTP/1.1 and a last 1.0, mixed-case names, spaces around values, Content-Length and chunked bodies with extensions and trailers, some ending in part of a request) fed whole, at random boundaries and a byte at a time (the 50 under 400 bytes), the requests read h11's on every cut and the bytes left those after them, the three paths' outputs the same bytes; 200 damaged streams fed whole as http_requests.py (the Python reference) reads them, 35 refused by both and 165 the same; module_specs in make check: 80 streams and their reading (tests/specs/http, no h11 at check time) whole on the three paths and cut 88 ways; bars: written, three workarounds (the bytes left in a list of one: bytes are no state; names and codings compared as text: bytes have no ==; field lines returned in a record: a fn returns no bare list), 159 lines against Python's 79, 278 ns a byte against the same algorithm in C 3.27 (85x), pure Python 67 (its bytes methods are C: Pure 4.1x slower), h11 284; 140's decision point after 027: bytes alone as a state (inflate, HTTP) and bytes' == (DNS, HTTP) now each met by two written specs, so the two-spec rule lets them into the core (their design not begun); a view calling a block fn met by one (DNS); aup's three its compiler's; CHECKPOINT's direction row
Diffstat (limited to 'research')
| -rw-r--r-- | research/experiments/044-specs/2026-10-19-http.jsonl | 5 | ||||
| -rw-r--r-- | research/experiments/044-specs/http_check.py | 201 | ||||
| -rw-r--r-- | research/experiments/044-specs/http_requests.py | 125 | ||||
| -rw-r--r-- | research/experiments/044-specs/http_time.c | 247 |
4 files changed, 578 insertions, 0 deletions
diff --git a/research/experiments/044-specs/2026-10-19-http.jsonl b/research/experiments/044-specs/2026-10-19-http.jsonl new file mode 100644 index 0000000..9f99101 --- /dev/null +++ b/research/experiments/044-specs/2026-10-19-http.jsonl @@ -0,0 +1,5 @@ +{"streams": 200, "bytes": 132623, "requests": 578, "right": {"whole": 200, "random": 200, "bytes": 50}, "paths": {"batches": 200, "rounds": 200, "all": 200}, "paths_same": true, "damaged": 200, "damaged_refused": 35, "damaged_agreed": 200, "aup_lines": 159, "aup_brace_lines": 24, "python_lines": 79} +{"streams":151,"bytes":98838,"requests":444,"jit":"0","batch":"","c_ns_per_byte":3.267,"pure_ns_per_byte":277.393,"to_c":84.9,"instructions":6066750,"instructions_per_byte":61.4} +{"streams":151,"bytes":98838,"requests":444,"jit":"0","batch":"0","c_ns_per_byte":3.281,"pure_ns_per_byte":279.531,"to_c":85.2,"instructions":6066750,"instructions_per_byte":61.4} +{"streams":151,"bytes":98838,"requests":444,"jit":"all","batch":"","c_ns_per_byte":3.274,"pure_ns_per_byte":278.113,"to_c":84.9,"instructions":6066750,"instructions_per_byte":61.4} +{"streams":151,"bytes":98838,"python_ns_per_byte":66.7,"h11_ns_per_byte":283.8} diff --git a/research/experiments/044-specs/http_check.py b/research/experiments/044-specs/http_check.py new file mode 100644 index 0000000..e3982c3 --- /dev/null +++ b/research/experiments/044-specs/http_check.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Research 140: HTTP/1.1 requests read a packet at a time, written in aup (aup's examples/http.aup). + +Builds it with aup (AUP, default the sibling checkout) and feeds it streams of pipelined requests (methods, targets, +HTTP/1.1 and a last HTTP/1.0, fields with mixed-case names and spaces around their values, bodies by Content-Length or +chunked with extensions and trailers, some streams ending in part of a request), each cut into packets: whole, at +random boundaries, and a byte at a time for the shorter ones, each packet an event. The requests read must be h11's (a +known implementation that reads as bytes come) and the bytes left those after the last of them, on every cut, and the +three paths (batches, rounds, all compiled) the same. Then damaged streams (cut short, a bit flipped) fed whole against http_requests.py, which reads +them the same way: the same requests and bytes left 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 http.aup and of +http_requests.py's algorithm. One JSON line. Given a path, it also writes the whole streams there for http_time.c; given a directory after it, Pure's +tests/specs/http there.""" +import json, os, random, subprocess, sys, tempfile + +import h11 + +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 http_requests # noqa: E402 + +TOKEN = "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + +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 streams(chooser, count): + def word(chars, low, high): + return ''.join(chooser.choices(chars, k=chooser.randrange(low, high))).encode() + + def ows(): + return chooser.choice([b'', b'', b' ', b'\t', b' \t']) + + def request(minor): + method = chooser.choice([b'GET', b'POST', b'PUT', b'DELETE', b'PATCH', b'OPTIONS', b'HEAD', word(TOKEN, 1, 8)]) + target = b'/' + bytes(chooser.randrange(0x21, 0x7f) for _ in range(chooser.randrange(0, 30))) + fields = [(chooser.choice([b'Host', b'host', b'HOST']), b'example.com')] + for _ in range(chooser.randrange(0, 6)): + name = word(TOKEN, 1, 12) + if name.lower() in (b'host', b'content-length', b'transfer-encoding', b'connection', b'expect', b'upgrade'): + continue + value = (b' ' if chooser.random() < 0.5 else b'\t').join( + bytes(chooser.randrange(0x21, 0x7f) for _ in range(chooser.randrange(1, 10))) for _ in range(chooser.randrange(0, 4))) + fields.append((name, value)) + chooser.shuffle(fields) + body, framing = b'', [] + pick = chooser.random() + if pick < 0.35: + body = bytes(chooser.randrange(256) for _ in range(chooser.randrange(0, 300))) + framing = [(b'Content-Length', str(len(body)).encode())] + elif pick < 0.65 and minor == 1: + framing = [(chooser.choice([b'Transfer-Encoding', b'transfer-encoding']), chooser.choice([b'chunked', b'Chunked']))] + chunks = [bytes(chooser.randrange(256) for _ in range(chooser.randrange(1, 120))) for _ in range(chooser.randrange(0, 5))] + for c in chunks: + size = (f'{len(c):x}' if chooser.random() < 0.5 else f'{len(c):X}').encode() + size = b'0' * chooser.choice([0, 0, 1, 3]) + size + tail = b';' + word('abc=', 0, 6) if chooser.random() < 0.3 else ows() + body += size + tail + b'\r\n' + c + b'\r\n' + trailers = b''.join(word(TOKEN, 1, 8) + b':' + ows() + word('xyz', 0, 6) + b'\r\n' for _ in range(chooser.randrange(0, 3))) + body += b'0' + (b';x' if chooser.random() < 0.2 else b'') + b'\r\n' + trailers + b'\r\n' + head = method + b' ' + target + b' HTTP/1.' + str(minor).encode() + b'\r\n' + head += b''.join(n + b':' + ows() + v + ows() + b'\r\n' for n, v in fields + framing) + return head + b'\r\n' + body + + out = [] + for _ in range(count): + stream = b''.join(request(1) for _ in range(chooser.randrange(1, 5))) + if chooser.random() < 0.2: + stream += request(0) # an HTTP/1.0 request ends its connection: last + elif chooser.random() < 0.3: + part = request(1) + stream += part[:chooser.randrange(1, len(part))] + out.append(stream) + return out + + +def cuts(chooser, stream): + """The stream whole, at random boundaries, and (if it is short) a byte at a time.""" + at = sorted(chooser.sample(range(1, len(stream)), min(len(stream) - 1, chooser.randrange(1, 12)))) + out = [[stream], [stream[a:b] for a, b in zip([0] + at, at + [len(stream)])]] + if len(stream) < 400: + out.append([stream[i:i + 1] for i in range(len(stream))]) + return out + + +def h11_reading(packets): + """The requests h11 reads whole from the packets, each as http_requests' tuples. (h11 hands a request's line, fields + and body on as they come, so the bytes left are those after the last whole request, as http_requests keeps them.)""" + conn, out, current = h11.Connection(h11.SERVER), [], None + for packet in packets: + conn.receive_data(packet) + while True: + event = conn.next_event() + if event in (h11.NEED_DATA, h11.PAUSED): + break + if isinstance(event, h11.Request): + current = [event.method, event.target, int(event.http_version[-1:]), list(event.headers), b'', []] + elif isinstance(event, h11.Data): + current[4] += event.data + elif isinstance(event, h11.EndOfMessage): + current[5] = list(event.headers) + out.append(tuple(current)) + conn.send(h11.Response(status_code=200, headers=[(b'content-length', b'0')])) + conn.send(h11.EndOfMessage()) + if conn.our_state is h11.DONE and conn.their_state is h11.DONE: + conn.start_next_cycle() + return out + + +def normal(requests): + """Requests with transfer-encoding's value lowercased, as h11 hands it on (that field alone).""" + return [(m, t, v, [(n, x.lower() if n == b'transfer-encoding' else x) for n, x in f], b, tr) for m, t, v, f, b, tr in requests] + + +def run(module, packets, env, work): + given = [] + for n, packet in enumerate(packets): + path = os.path.join(work, f'{n}.packet') + with open(path, 'wb') as f: + f.write(packet) + given.append(f'message packet @{path}\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=600).stdout.splitlines() + state = json.loads(out[-1]) + s = state['states'] + pairs = lambda fs: [(bytes.fromhex(f['name']), bytes.fromhex(f['value'])) for f in fs] + requests = [(bytes.fromhex(r['method']), bytes.fromhex(r['target']), r['minor'], pairs(r['fields']), bytes.fromhex(r['body']), + pairs(r['trailers'])) for r in s['requests']] + left = bytes.fromhex(s['pending'][0]['data']) if s['pending'] else b'' + return out, (requests, left), any(json.loads(line)['fault'] != 'none' for line in out) + + +chooser = random.Random(140) +made = streams(chooser, 200) +if len(sys.argv) > 1: # the whole streams (no request left in part) for http_time.c: each a u32 big-endian length, then it + with open(sys.argv[1], 'wb') as f: + f.write(b''.join(len(s).to_bytes(4, 'big') + s for s in made if not http_requests.feed(b'', s)[1])) +if len(sys.argv) > 2: # Pure's tests/specs/http: the first 80 streams so, and what http_requests (h11's reading) reads + with open(os.path.join(sys.argv[2], 'streams.bin'), 'wb') as f: + f.write(b''.join(len(s).to_bytes(4, 'big') + s for s in made[:80])) + hexed = lambda fs: [[n.hex(), v.hex()] for n, v in fs] + with open(os.path.join(sys.argv[2], 'read.json'), 'w') as f: + json.dump([{'requests': [[m.hex(), t.hex(), v, hexed(fs), b.hex(), hexed(tr)] for m, t, v, fs, b, tr in got], 'left': left.hex()} + for got, left in (http_requests.feed(b'', s) for s in made[:80])], f, separators=(',', ':')) + f.write('\n') +paths = {'batches': 0, 'rounds': 0, 'all': 0} +fed = {'whole': 0, 'random': 0, 'bytes': 0} +same_paths = True +with tempfile.TemporaryDirectory() as work: + module = os.path.join(work, 'http.pmod') + subprocess.run([os.path.join(aup, 'build', 'aup'), 'build', os.path.join(aup, 'examples', 'http.aup'), '-o', module], + check=True) + for stream in made: + for kind, packets in zip(('whole', 'random', 'bytes'), cuts(chooser, stream)): + want = http_requests.feed(b'', stream) + assert normal(want[0]) == h11_reading(packets), 'the reference differs from h11' + envs = (('batches', {'PURE_JIT': '0'}), ('rounds', {'PURE_JIT': '0', 'PURE_BATCH': '0'}), ('all', {'PURE_JIT': 'all'})) \ + if kind == 'random' else (('batches', {'PURE_JIT': '0'}),) + raws = [] + for name, env in envs: + raw, got, failed = run(module, packets, env, work) + right = not failed and got == want + fed[kind] += right and name == 'batches' + if kind == 'random': + paths[name] += right + raws.append(raw) + same_paths = same_paths and all(r == raws[0] for r in raws) + broken = [] + for stream in chooser.sample(made, 100): + flipped = bytearray(stream) + flipped[chooser.randrange(len(stream))] ^= 1 << chooser.randrange(8) + broken += [stream[:chooser.randrange(1, len(stream))], bytes(flipped)] + agreed = refused = 0 + for stream in broken: + try: + want = http_requests.feed(b'', stream) + except ValueError: + want = None + refused += want is None + _, got, failed = run(module, [stream], {}, work) + agreed += (want is None and failed) or (want is not None and not failed and got == want) +aup_code, aup_braces = lines(os.path.join(aup, 'examples', 'http.aup')) +python_code, _ = lines(os.path.join(here, 'http_requests.py'), stop='if __name__') +ok = all(n == len(made) for n in paths.values()) and fed['whole'] == fed['random'] == len(made) and same_paths and \ + fed['bytes'] == sum(len(s) < 400 for s in made) and agreed == len(broken) +print(json.dumps({'streams': len(made), 'bytes': sum(map(len, made)), + 'requests': sum(len(http_requests.feed(b'', s)[0]) for s in made), 'right': fed, 'paths': 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/http_requests.py b/research/experiments/044-specs/http_requests.py new file mode 100644 index 0000000..20b0fb9 --- /dev/null +++ b/research/experiments/044-specs/http_requests.py @@ -0,0 +1,125 @@ +# HTTP/1.1 requests (RFC 9112) read a packet at a time in plain Python: the reference 140 counts aup's lines against. +# Each packet's bytes are added to what the last ones left, and every request they complete is read: its line, its +# fields (names lowercased), its body by Content-Length or chunked, a chunked one's trailers. The grammar is h11's. +TOKEN = set(b"!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +HEX = set(b'0123456789abcdefABCDEF') +SPACE = set(b'\0\t\n\x0b\x0c\r ') # never in a field value's words + + +def line(d, i): + """The line from i to the next CRLF, and where the next starts; None, None if it has not come yet.""" + j = d.find(b'\r\n', i) + return (None, None) if j < 0 else (d[i:j], j + 2) + + +def field(text): + name, colon, value = text.partition(b':') + value = value.strip(b' \t') + if not colon or not name or not set(name) <= TOKEN or any(b in SPACE and b not in b' \t' for b in value): + raise ValueError('a field') + return name.lower(), value + + +def fields(d, i): + """The field lines from i to an empty line, and where it ends; None, None if they have not all come.""" + out = [] + while True: + text, i = line(d, i) + if text is None or not text: + return (None, None) if text is None else (out, i) + out.append(field(text)) + + +def request(d): + """The request at the start of d and its length; None if d holds only part of one.""" + text, i = line(d, 0) + if text is None: + return None + parts = text.split(b' ') + if len(parts) != 3 or not parts[0] or not set(parts[0]) <= TOKEN or not parts[1] or \ + any(b < 0x21 or b > 0x7e for b in parts[1]) or parts[2] not in (b'HTTP/1.0', b'HTTP/1.1'): + raise ValueError('a request line') + method, target, minor = parts[0], parts[1], parts[2][-1] - 0x30 + found, i = fields(d, i) + if found is None: + return None + named = lambda n: [v for k, v in found if k == n] + hosts, coded, length = named(b'host'), named(b'transfer-encoding'), named(b'content-length') + if len(hosts) > 1 or (minor == 1 and not hosts): + raise ValueError('a host') + body, trailers = b'', [] + if coded: + if length or len(coded) > 1 or coded[0].lower() != b'chunked': + raise ValueError('a coding') + while True: + text, i = line(d, i) + if text is None: + return None + size, semi, ext = text.partition(b';') + size = size if semi else size.rstrip(b' \t') + if not 1 <= len(size) <= 20 or not set(size) <= HEX or b'\n' in ext or int(size, 16) > 2 ** 31: + raise ValueError('a chunk size') # 2^31 at most: ours, not h11's + n = int(size, 16) + if n == 0: + trailers, i = fields(d, i) + if trailers is None: + return None + break + if len(d) < i + n + 2: + return None + if d[i + n:i + n + 2] != b'\r\n': + raise ValueError('a chunk end') + body += d[i:i + n] + i += n + 2 + elif length: + if len(length) > 1 or not length[0].isdigit() or int(length[0]) > 2 ** 31: + raise ValueError('a length') + if len(d) < i + int(length[0]): + return None + body, i = d[i:i + int(length[0])], i + int(length[0]) + return (method, target, minor, found, body, trailers), i + + +def feed(pending, packet): + """The requests the packet completes, and the bytes left for the next.""" + d, out = pending + packet, [] + while d: + got = request(d) + if got is None: + break + out.append(got[0]) + d = d[got[1]:] + return out, d + + +if __name__ == '__main__': + import sys, time + import h11 + with open(sys.argv[1], 'rb') as f: # http_check.py's whole streams: each a u32 big-endian length, then its bytes + given = f.read() + streams, at = [], 0 + while at < len(given): + streams.append(given[at + 4:at + 4 + int.from_bytes(given[at:at + 4], 'big')]) + at += 4 + len(streams[-1]) + size = sum(map(len, streams)) + + def by_h11(stream): # h11 as a server reads each request, answers it, and goes on + conn = h11.Connection(h11.SERVER) + conn.receive_data(stream) + while (event := conn.next_event()) not in (h11.NEED_DATA, h11.PAUSED): + if isinstance(event, h11.EndOfMessage): + conn.send(h11.Response(status_code=200, headers=[(b'content-length', b'0')])) + conn.send(h11.EndOfMessage()) + if conn.our_state is h11.DONE and conn.their_state is h11.DONE: + conn.start_next_cycle() + + times = {} + for name, read in (('python', lambda s: feed(b'', s)), ('h11', by_h11)): + runs = [] + for _ in range(7): + start = time.perf_counter() + for s in streams: + read(s) + runs.append((time.perf_counter() - start) * 1e9 / size) + times[name] = sorted(runs)[3] + print('{"streams":%d,"bytes":%d,"python_ns_per_byte":%.1f,"h11_ns_per_byte":%.1f}' % (len(streams), size, times['python'], times['h11'])) diff --git a/research/experiments/044-specs/http_time.c b/research/experiments/044-specs/http_time.c new file mode 100644 index 0000000..51bd595 --- /dev/null +++ b/research/experiments/044-specs/http_time.c @@ -0,0 +1,247 @@ +/* Research 140: HTTP/1.1 requests read a packet at a time in aup (aup's examples/http.aup, run by Pure) against the + * same algorithm in C, on the same streams (each whole, one packet). No C library for it is on the machine, so the + * world's reference is h11 in Python (http_check.py's oracle). C's and Pure's requests must be as many as the streams + * hold, none left over; then each is timed (the median of seven runs of rounds passes). + * + * cc -O2 -std=c17 -I../../../core http_time.c ../../../build/libpure.a -lm -pthread -o http_time + * ./http_time http.pmod STREAMS ROUNDS (STREAMS: each a u32 big-endian length, then its bytes, all whole) + * + * One JSON line: the streams, their bytes and requests, the nanoseconds per byte of each, Pure's ratio and the + * instructions Pure counted for one pass. */ +#define _POSIX_C_SOURCE 200809L +#include "module.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +/* ---- the same algorithm in C */ +typedef struct { size_t name, name_size, value, value_size; } Field; +typedef struct { + size_t method, method_size, target, target_size; + int minor; + Field fields[64], trailers[16]; + int count, trailer_count; + unsigned char lowered[2048]; /* the names lowercased */ + size_t lowered_used; + unsigned char body[1 << 16]; + size_t body_size; +} Request; + +static bool token(unsigned char b) { + return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || (b && strchr("!#$%&'*+-.^_`|~", b)); +} +static bool hex(unsigned char b) { return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F'); } +static bool space(unsigned char b) { return !b || (b >= 9 && b <= 13) || b == ' '; } +static bool blank(unsigned char b) { return b == ' ' || b == '\t'; } + +static long crlf(const unsigned char *d, size_t size, size_t i) { + for (size_t j = i; j + 1 < size; ++j) + if (d[j] == '\r' && d[j + 1] == '\n') return (long)j; + return -1; +} + +/* The field line d[i .. e] into f (its name lowercased into r's room); false if it is none. */ +static bool field(const unsigned char *d, size_t i, size_t e, Request *r, Field *f) { + size_t c = i; + while (c < e && d[c] != ':') ++c; + if (c == i || c == e || r->lowered_used + (c - i) > sizeof(r->lowered)) return false; + f->name = r->lowered_used, f->name_size = c - i; + for (size_t k = i; k < c; ++k) { + if (!token(d[k])) return false; + r->lowered[r->lowered_used++] = d[k] >= 'A' && d[k] <= 'Z' ? (unsigned char)(d[k] + 32) : d[k]; + } + size_t a = c + 1, b = e; + while (a < b && blank(d[a])) ++a; + while (b > a && blank(d[b - 1])) --b; + for (size_t k = a; k < b; ++k) + if (space(d[k]) && !blank(d[k])) return false; + f->value = a, f->value_size = b - a; + return true; +} + +/* The field lines from i into fields; where they end, -1 if they have not all come, -2 if they are none. */ +static long fields(const unsigned char *d, size_t size, size_t i, Request *r, Field *into, int room, int *count) { + *count = 0; + for (;;) { + long e = crlf(d, size, i); + if (e < 0) return -1; + if ((size_t)e == i) return e + 2; + if (*count == room || !field(d, i, (size_t)e, r, &into[(*count)++])) return -2; + i = (size_t)e + 2; + } +} + +static bool named(const Request *r, const Field *f, const char *name) { + return f->name_size == strlen(name) && !memcmp(r->lowered + f->name, name, f->name_size); +} + +/* The request at the start of d and its length: -1 if d holds only part of one, -2 if it is none. */ +static long request(const unsigned char *d, size_t size, Request *r) { + long e = crlf(d, size, 0); + if (e < 0) return -1; + size_t s1 = 0, s2; + while ((long)s1 < e && d[s1] != ' ') ++s1; + for (s2 = s1 + 1; (long)s2 < e && d[s2] != ' '; ++s2) {} + if (!s1 || s2 <= s1 + 1 || (long)s2 + 9 != e || memcmp(d + s2 + 1, "HTTP/1.", 7) || (d[e - 1] != '0' && d[e - 1] != '1')) + return -2; + for (size_t k = 0; k < s1; ++k) if (!token(d[k])) return -2; + for (size_t k = s1 + 1; k < s2; ++k) if (d[k] < 0x21 || d[k] > 0x7e) return -2; + r->method = 0, r->method_size = s1, r->target = s1 + 1, r->target_size = s2 - s1 - 1, r->minor = d[e - 1] - '0'; + r->lowered_used = 0, r->body_size = 0, r->trailer_count = 0; + long i = fields(d, size, (size_t)e + 2, r, r->fields, 64, &r->count); + if (i < 0) return i; + int hosts = 0, coded = 0, lengths = 0; + bool chunked = false; + double length = 0; + for (int k = 0; k < r->count; ++k) { + const Field *f = &r->fields[k]; + if (named(r, f, "host")) ++hosts; + if (named(r, f, "transfer-encoding")) { + ++coded; + chunked = f->value_size == 7 && !strncasecmp((const char *)d + f->value, "chunked", 7); + } + if (named(r, f, "content-length")) { + ++lengths; + if (!f->value_size) return -2; + for (size_t b = 0; b < f->value_size; ++b) { + if (d[f->value + b] < '0' || d[f->value + b] > '9') return -2; + length = length * 10 + (d[f->value + b] - '0'); + } + } + } + if (hosts > 1 || (!hosts && r->minor == 1)) return -2; + size_t j = (size_t)i; + if (coded) { + if (lengths || coded > 1 || !chunked) return -2; + for (;;) { + long c = crlf(d, size, j); + if (c < 0) return -1; + size_t semi = j, z; + while ((long)semi < c && d[semi] != ';') ++semi; + z = semi; + if ((long)semi == c) while (z > j && blank(d[z - 1])) --z; + if (z == j || z - j > 20) return -2; + double n = 0; + for (size_t k = j; k < z; ++k) { + if (!hex(d[k])) return -2; + n = n * 16 + (d[k] <= '9' ? d[k] - '0' : d[k] >= 'a' ? d[k] - 'a' + 10 : d[k] - 'A' + 10); + } + for (size_t k = semi; (long)k < c; ++k) if (d[k] == '\n') return -2; + if (n > 2147483648.0) return -2; + if (n == 0) { + long after = fields(d, size, (size_t)c + 2, r, r->trailers, 16, &r->trailer_count); + if (after < 0) return after; + return after; + } + size_t m = (size_t)n; + if (size < (size_t)c + 4 + m) return -1; + if (d[c + 2 + m] != '\r' || d[c + 3 + m] != '\n' || r->body_size + m > sizeof(r->body)) return -2; + memcpy(r->body + r->body_size, d + c + 2, m); + r->body_size += m; + j = (size_t)c + 4 + m; + } + } + if (lengths) { + if (lengths > 1 || length > 2147483648.0) return -2; + if (size < j + (size_t)length) return -1; + if ((size_t)length > sizeof(r->body)) return -2; + memcpy(r->body, d + j, (size_t)length); + r->body_size = (size_t)length; + j += (size_t)length; + } + return (long)j; +} + +static unsigned char pending[1 << 20]; +static size_t pending_size; +static Request current; + +/* The requests a packet completes (counted), the rest kept; -1 if the stream is no HTTP. */ +static long feed(const unsigned char *packet, size_t size) { + if (pending_size + size > sizeof(pending)) return -1; + memcpy(pending + pending_size, packet, size); + pending_size += size; + long count = 0, n; + size_t at = 0; + while (at < pending_size && (n = request(pending + at, pending_size - at, ¤t)) >= 0) at += (size_t)n, ++count; + if (at < pending_size && n == -2) return -1; + memmove(pending, pending + at, pending_size - at); + pending_size -= at; + return count; +} + +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); +} + +int main(int argc, char **argv) { + if (argc != 4) { fprintf(stderr, "usage: %s MODULE STREAMS 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), bytes = 0; + fclose(f); + int rounds = atoi(argv[3]), count = 0; + static size_t starts[65536], sizes[65536]; + for (size_t at = 0; at + 4 <= size && count < 65536; at += 4 + sizes[count++]) { + starts[count] = at + 4; + sizes[count] = (size_t)all[at] << 24 | (size_t)all[at + 1] << 16 | (size_t)all[at + 2] << 8 | all[at + 3]; + 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, "packet"), requests = pure_module_state(&module, "requests"), + left = pure_module_state(&module, "pending"); + long c_requests = 0; + uint64_t work = 0; + for (int s = 0; s < count; ++s) { + long got = feed(all + starts[s], sizes[s]); + if (got < 0 || pending_size) { fprintf(stderr, "stream %d: C read no whole requests\n", s); return 1; } + c_requests += got; + if (pure_app_message(&app, (uint32_t)input, (PureSpan){all + starts[s], sizes[s]}) != PURE_OK) { + fprintf(stderr, "stream %d: Pure's event failed\n", s); + return 1; + } + work += app.work; + } + if ((long)pure_list_count(app.lists[requests]) != c_requests || pure_list_count(app.lists[left])) { + fprintf(stderr, "Pure read %zu requests, C %ld\n", pure_list_count(app.lists[requests]), c_requests); + return 1; + } + + double c_runs[7], pure_runs[7]; + for (int r = 0; r < 7; ++r) { + double start = now_ns(); + for (int i = 0; i < rounds; ++i) + for (int s = 0; s < count; ++s) feed(all + starts[s], sizes[s]); + c_runs[r] = (now_ns() - start) / rounds; + start = now_ns(); + for (int i = 0; i < rounds; ++i) + for (int s = 0; s < count; ++s) + if (pure_app_message(&app, (uint32_t)input, (PureSpan){all + starts[s], sizes[s]}) != PURE_OK) return 1; + pure_runs[r] = (now_ns() - start) / rounds; + } + qsort(c_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("{\"streams\":%d,\"bytes\":%zu,\"requests\":%ld,\"jit\":\"%s\",\"batch\":\"%s\",\"c_ns_per_byte\":%.3f," + "\"pure_ns_per_byte\":%.3f,\"to_c\":%.1f,\"instructions\":%llu,\"instructions_per_byte\":%.1f}\n", + count, bytes, c_requests, jit ? jit : "", batch ? batch : "", c_runs[3] / n, pure_runs[3] / n, + pure_runs[3] / c_runs[3], (unsigned long long)work, (double)work / n); + pure_app_stop(&app); + return 0; +} |
