From c789346b70ee18f95477e99d8c350bf3fddfb606 Mon Sep 17 00:00:00 2001 From: praisethemoon Date: Sun, 17 May 2026 13:15:05 +0200 Subject: [PATCH 01/11] typev: add baseline, json and limited-conn tests --- frameworks/typev/README.md | 6 +- .../bundle/benchmark-code/src/dataset.tc | 142 +++++ .../typev/bundle/benchmark-code/src/http.tc | 319 +++++++++++ .../typev/bundle/benchmark-code/src/main.tc | 117 +++- .../bundle/benchmark-code/src/response.tc | 23 + .../typev/bundle/benchmark-code/stdlib/fs.tc | 509 ++++++++++++++++++ .../benchmark-code/stdlib/internal/file.tc | 71 +++ .../benchmark-code/stdlib/internal/fs.tc | 149 +++++ frameworks/typev/bundle/output.tvbc | Bin 5127 -> 13359 bytes frameworks/typev/meta.json | 5 +- 10 files changed, 1314 insertions(+), 27 deletions(-) create mode 100644 frameworks/typev/bundle/benchmark-code/src/dataset.tc create mode 100644 frameworks/typev/bundle/benchmark-code/src/http.tc create mode 100644 frameworks/typev/bundle/benchmark-code/src/response.tc create mode 100755 frameworks/typev/bundle/benchmark-code/stdlib/fs.tc create mode 100644 frameworks/typev/bundle/benchmark-code/stdlib/internal/file.tc create mode 100755 frameworks/typev/bundle/benchmark-code/stdlib/internal/fs.tc diff --git a/frameworks/typev/README.md b/frameworks/typev/README.md index 3f8ae6367..d1c741f5d 100644 --- a/frameworks/typev/README.md +++ b/frameworks/typev/README.md @@ -4,8 +4,8 @@ Type-V is a bytecode-interpreting concurrent VM, using io_uring socket I/O. The benchmark server is written in Type-C (typev's own language) and compiled ahead of time to bytecode that the VM runs. -- **Tier:** tuned -- **Profile:** pipelined +- **Tier:** engine +- **Profiles:** pipelined, baseline, limited-conn, json ## Layout @@ -14,7 +14,7 @@ compiled ahead of time to bytecode that the VM runs. | `Dockerfile` | Downloads the prebuilt typev VM, runs the server | | `meta.json` | Framework metadata | | `bundle/output.tvbc` | The compiled benchmark server | -| `bundle/benchmark-code/` | Benchmark source — Type-C `main.tc` plus the `std.io` / `std.socket` modules it uses | +| `bundle/benchmark-code/` | Benchmark source — a Type-C module: `src/` (server, HTTP parser, response builders) plus the `std.io` / `std.socket` modules it uses | The typev VM itself (binary + FFI plugins) is not vendored here — the Dockerfile fetches it from object storage at build time. Only the compiled benchmark and diff --git a/frameworks/typev/bundle/benchmark-code/src/dataset.tc b/frameworks/typev/bundle/benchmark-code/src/dataset.tc new file mode 100644 index 000000000..9ed69ac0f --- /dev/null +++ b/frameworks/typev/bundle/benchmark-code/src/dataset.tc @@ -0,0 +1,142 @@ +// Dataset loading + the JSON-processing profile response. +// +// The json profile serves GET /json/{count}?m=M from a fixed 50-item dataset +// (a JSON array). loadDataset() reads that file once and splits it into its +// item objects; jsonResponse() then emits the first `count` items per request +// with total = price * quantity * m spliced in — serialized fresh each call, +// no cached response. + +from std.fs import readText + +// One loaded dataset. prefixes[i] is item i's JSON text WITHOUT its closing +// '}', so a per-request `,"total":N}` can be appended. prices/quantities feed +// the total. n is the number of items loaded. +type Dataset = struct { + prefixes: string[], + prices: int[], + quantities: int[], + n: int +} + +// Within data[start..end), find `key` and return the decimal integer that +// follows it (after an optional run of spaces). 0 if not found. +local fn fieldInt(data: byte[], start: uint, end: uint, key: byte[]) -> int { + let kl: uint = key.length + if kl == 0u || end < start + kl { + return 0 + } + let limit: uint = end - kl + foreach i: uint in start, limit + 1u { + let hit: bool = true + foreach k: uint in 0u, kl { + if (data[i + k] as int) != (key[k] as int) { + hit = false + break + } + } + if hit { + let p: uint = i + kl + while p < end && (data[p] as int) == 32 { + p = p + 1u + } + let v: int = 0 + while p < end { + let d: int = data[p] as int + if d < 48 || d > 57 { + break + } + v = v * 10 + (d - 48) + p = p + 1u + } + return v + } + } + return 0 +} + +// Read the dataset file and split the top-level JSON array into its objects. +fn loadDataset(path: string) -> Dataset { + let ds: Dataset = {prefixes: [], prices: [], quantities: [], n: 0} + let (txt, st) = readText(path) + if st < 0 { + return ds + } + let data: byte[] = txt.bytes() + let len: uint = data.length + let priceKey: byte[] = "\"price\":".bytes() + let qtyKey: byte[] = "\"quantity\":".bytes() + + // skip to the opening '[' + let p: uint = 0u + while p < len && (data[p] as int) != 91 { + p = p + 1u + } + p = p + 1u + + let count: int = 0 + while p < len { + // skip whitespace and commas between items + while p < len { + let c: int = data[p] as int + if c == 32 || c == 10 || c == 13 || c == 9 || c == 44 { + p = p + 1u + } else { + break + } + } + if p >= len || (data[p] as int) == 93 { + break + } + // data[p] is '{' — scan to the matching '}', string-aware + let itemStart: uint = p + let depth: int = 0 + let inStr: bool = false + while p < len { + let c: int = data[p] as int + if inStr { + if c == 92 { + p = p + 1u + } else if c == 34 { + inStr = false + } + } else { + if c == 34 { + inStr = true + } else if c == 123 { + depth = depth + 1 + } else if c == 125 { + depth = depth - 1 + } + } + p = p + 1u + if !inStr && depth == 0 { + break + } + } + let itemEnd: uint = p + ds.prefixes.push(txt.substring(itemStart, itemEnd - 1u)) + ds.prices.push(fieldInt(data, itemStart, itemEnd, priceKey)) + ds.quantities.push(fieldInt(data, itemStart, itemEnd, qtyKey)) + count = count + 1 + } + ds.n = count + return ds +} + +// Build the json-profile response: first `count` dataset items, each with +// total = price * quantity * mult, plus the count field. +fn jsonResponse(ds: Dataset, count: int, mult: int, keepAlive: bool) -> string { + let limit: int = if count > ds.n => ds.n else count + let items: string = "" + foreach i in 0, limit { + let total: int = ds.prices[i as uint] * ds.quantities[i as uint] * mult + if i > 0 { + items = items + "," + } + items = items + ds.prefixes[i as uint] + ",\"total\":" + total + "}" + } + let body: string = "{\"items\":[" + items + "],\"count\":" + limit + "}" + let blen: int = body.bytes().length as int + let conn: string = if keepAlive => "keep-alive" else "close" + return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " + blen + "\r\nConnection: " + conn + "\r\n\r\n" + body +} diff --git a/frameworks/typev/bundle/benchmark-code/src/http.tc b/frameworks/typev/bundle/benchmark-code/src/http.tc new file mode 100644 index 000000000..aaa07cfcb --- /dev/null +++ b/frameworks/typev/bundle/benchmark-code/src/http.tc @@ -0,0 +1,319 @@ +// HTTP/1.1 request parsing for the typev HttpArena server. +// +// parseRequest() reads exactly one request from the front of an accumulating +// byte buffer. It returns complete=false when the request has not fully +// arrived yet (TCP fragmentation, or a pipelined batch still in flight), so +// the caller can recv() more bytes and retry. GET and POST are supported, +// with both Content-Length and chunked transfer-encoding bodies. + +// A parsed request. `consumed` is the number of buffer bytes this request +// occupies, so the caller can drop them and parse the next one. +type Req = struct { + complete: bool, + consumed: uint, + isPipeline: bool, + isBaseline: bool, + isJson: bool, + isPost: bool, + keepAlive: bool, + result: int, + jcount: int, + jmult: int +} + +// Result of decoding a chunked body. +type Chunk = struct { + ok: bool, + value: int, + endPos: uint +} + +// Parse a decimal integer from buf[start..end), ignoring non-digit bytes. +local fn parseDigits(buf: byte[], start: uint, end: uint) -> int { + let v: int = 0 + let p: uint = start + while p < end { + let d: int = buf[p] as int + if d >= 48 && d <= 57 { + v = v * 10 + (d - 48) + } + p = p + 1u + } + return v +} + +// Case-insensitive match of `name` against the header name at buf[ls..), +// requiring it to be immediately followed by ':'. +local fn matchHeader(buf: byte[], ls: uint, le: uint, name: string) -> bool { + let nb = name.bytes() + let nl: uint = nb.length + if ls + nl >= le { + return false + } + foreach k: uint in 0u, nl { + let c: int = buf[ls + k] as int + if c >= 65 && c <= 90 { + c = c + 32 + } + if c != (nb[k] as int) { + return false + } + } + return (buf[ls + nl] as int) == 58 +} + +// Integer value of a header line's value (the part after ':'). +local fn headerNumber(buf: byte[], ls: uint, le: uint) -> int { + let p: uint = ls + while p < le && (buf[p] as int) != 58 { + p = p + 1u + } + p = p + 1u + return parseDigits(buf, p, le) +} + +// Case-insensitive substring search for `lit` within buf[start..end). +local fn containsCI(buf: byte[], start: uint, end: uint, lit: string) -> bool { + let lb = lit.bytes() + let ll: uint = lb.length + if ll == 0u { + return true + } + if end < start + ll { + return false + } + let limit: uint = end - ll + foreach i: uint in start, limit + 1u { + let m: bool = true + foreach k: uint in 0u, ll { + let c: int = buf[i + k] as int + if c >= 65 && c <= 90 { + c = c + 32 + } + if c != (lb[k] as int) { + m = false + break + } + } + if m { + return true + } + } + return false +} + +// Decode a chunked body starting at buf[start], summing the decimal digits of +// the decoded data. ok=false if the body has not fully arrived yet. +local fn decodeChunked(buf: byte[], start: uint, len: uint) -> Chunk { + let res: Chunk = {ok: false, value: 0, endPos: 0u} + let p: uint = start + let acc: int = 0 + while true { + if p >= len { + return res + } + let size: int = 0 + let any: bool = false + while p < len { + let c: int = buf[p] as int + let hv: int = -1 + if c >= 48 && c <= 57 { + hv = c - 48 + } else if c >= 97 && c <= 102 { + hv = c - 87 + } else if c >= 65 && c <= 70 { + hv = c - 55 + } + if hv < 0 { + break + } + size = size * 16 + hv + any = true + p = p + 1u + } + if !any { + return res + } + if p + 1u >= len { + return res + } + if (buf[p] as int) != 13 || (buf[p + 1u] as int) != 10 { + return res + } + p = p + 2u + if size == 0 { + if p + 1u >= len { + return res + } + res.ok = true + res.value = acc + res.endPos = p + 2u + return res + } + let sz: uint = size as uint + if p + sz + 2u > len { + return res + } + foreach q: uint in p, p + sz { + let d: int = buf[q] as int + if d >= 48 && d <= 57 { + acc = acc * 10 + (d - 48) + } + } + p = p + sz + 2u + } + return res +} + +// Parse one HTTP request from the front of buf[0..len). +fn parseRequest(buf: byte[], len: uint) -> Req { + let r: Req = {complete: false, consumed: 0u, isPipeline: false, isBaseline: false, isJson: false, isPost: false, keepAlive: true, result: 0, jcount: 0, jmult: 0} + if len < 4u { + return r + } + + // end of headers ("\r\n\r\n") + let headerEnd: int = -1 + foreach i: uint in 0u, len - 3u { + if (buf[i] as int) == 13 && (buf[i + 1u] as int) == 10 && (buf[i + 2u] as int) == 13 && (buf[i + 3u] as int) == 10 { + headerEnd = i as int + break + } + } + if headerEnd < 0 { + return r + } + let he: uint = headerEnd as uint + let bodyStart: uint = he + 4u + + // first space -> start of the request target ('/...') + let sp: uint = 0u + while sp < he && (buf[sp] as int) != 32 { + sp = sp + 1u + } + let pathStart: uint = sp + 1u + + // method: POST is the 4-char method, GET the 3-char one — so the first + // space position equals the method length. Stored on the struct: a + // long-lived `let bool` holding a comparison miscompiles under -O. + r.isPost = sp == 4u + + // route on the character after '/': 'p'(112)=pipeline, 'b'(98)=baseline + let rc: int = if pathStart + 1u < he => (buf[pathStart + 1u] as int) else 0 + r.isPipeline = rc == 112 + r.isBaseline = rc == 98 + r.isJson = rc == 106 + + // end of request line (first CR) + let lineEnd: uint = pathStart + while lineEnd < he && (buf[lineEnd] as int) != 13 { + lineEnd = lineEnd + 1u + } + + // header scan + let contentLen: int = -1 + let chunked: bool = false + let h: uint = lineEnd + 2u + while h < he { + let le: uint = h + while le < he && (buf[le] as int) != 13 { + le = le + 1u + } + if matchHeader(buf, h, le, "content-length") { + contentLen = headerNumber(buf, h, le) + } else if matchHeader(buf, h, le, "transfer-encoding") { + if containsCI(buf, h, le, "chunked") { + chunked = true + } + } else if matchHeader(buf, h, le, "connection") { + if containsCI(buf, h, le, "close") { + r.keepAlive = false + } + } + h = le + 2u + } + + // body + let bodyVal: int = 0 + let consumed: uint = bodyStart + if r.isPost { + if chunked { + let dec = decodeChunked(buf, bodyStart, len) + if !dec.ok { + return r + } + bodyVal = dec.value + consumed = dec.endPos + } else { + let cl: uint = if contentLen > 0 => (contentLen as uint) else 0u + if len < bodyStart + cl { + return r + } + bodyVal = parseDigits(buf, bodyStart, bodyStart + cl) + consumed = bodyStart + cl + } + } + + r.complete = true + r.consumed = consumed + + if r.isBaseline { + // sum the first two digit-runs in the query (after '?') + let q: uint = pathStart + while q < lineEnd && (buf[q] as int) != 63 { + q = q + 1u + } + let sum: int = 0 + let cnt: int = 0 + let p: uint = q + while p < lineEnd && cnt < 2 { + let c: int = buf[p] as int + if c >= 48 && c <= 57 { + let v: int = 0 + while p < lineEnd { + let d: int = buf[p] as int + if d < 48 || d > 57 { + break + } + v = v * 10 + (d - 48) + p = p + 1u + } + sum = sum + v + cnt = cnt + 1 + } else { + p = p + 1u + } + } + r.result = sum + bodyVal + } + + if r.isJson { + // /json/{count}?m=M — count and m are the first two digit-runs of the target + let cnt: int = 0 + let p: uint = pathStart + while p < lineEnd && cnt < 2 { + let c: int = buf[p] as int + if c >= 48 && c <= 57 { + let v: int = 0 + while p < lineEnd { + let d: int = buf[p] as int + if d < 48 || d > 57 { + break + } + v = v * 10 + (d - 48) + p = p + 1u + } + if cnt == 0 { + r.jcount = v + } else { + r.jmult = v + } + cnt = cnt + 1 + } else { + p = p + 1u + } + } + } + + return r +} diff --git a/frameworks/typev/bundle/benchmark-code/src/main.tc b/frameworks/typev/bundle/benchmark-code/src/main.tc index f00a9c0d4..9ab6d79a8 100644 --- a/frameworks/typev/bundle/benchmark-code/src/main.tc +++ b/frameworks/typev/bundle/benchmark-code/src/main.tc @@ -1,37 +1,108 @@ -from std.io import println -from std.socket import tcp_listen, tcp_accept, tcp_read, tcp_write, tcp_close, count_http_terminators +// typev HttpArena server — HTTP/1.1, profiles: pipelined + baseline. +// +// A single incremental-parser server: each connection runs in its own +// lightweight process, accumulating bytes across recv() calls so it survives +// pipelined batches and TCP-fragmented requests. Parsing lives in http.tc, +// response formatting in response.tc. -let const RESPONSE: string = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok" +from std.io import println +from std.socket import tcp_listen, tcp_accept, tcp_read, tcp_write, tcp_close +from http import parseRequest +from response import pipelineResponse, baselineResponse +from dataset import loadDataset, jsonResponse -let const MAX_DEPTH: uint = 256u +// Per-connection receive buffer capacity. +let const BUFCAP: uint = 65536u -let const READ_BUF: uint = 262144u +// Dataset for the json profile — the container mounts it at /data/dataset.json. +let const DATASET_PATH: string = "/data/dataset.json" -fn handle_client(client_fd: int) -> void { +// Serve one connection: read, parse every complete request in the buffer, +// write the batched responses, compact the leftover, repeat. +fn handle_client(fd: int) -> void { let buf: byte[] = [] - buf.resize(READ_BUF) - - let resp_len: uint = RESPONSE.bytes().length - let batch: byte[] = RESPONSE.repeat(MAX_DEPTH).bytes() - - while true { - let n = tcp_read(client_fd, buf, READ_BUF) - if n <= 0 { break } - - let count = count_http_terminators(buf, n as uint) - let reply: uint = if count == 0u => 1u else if count > MAX_DEPTH => MAX_DEPTH else count + buf.resize(BUFCAP) + let tmp: byte[] = [] + tmp.resize(BUFCAP) + let len: uint = 0u + let running: bool = true + + // Dataset for the json profile — loaded once per connection. + let ds = loadDataset(DATASET_PATH) + + while running { + let got: int = 0 + if len == 0u { + got = tcp_read(fd, buf, BUFCAP) + } else { + // leftover present — read into a scratch buffer, then append + got = tcp_read(fd, tmp, BUFCAP - len) + if got > 0 { + let g: uint = got as uint + foreach k: uint in 0u, g { + buf[len + k] = tmp[k] + } + } + } - let written = tcp_write(client_fd, batch, reply * resp_len) - if written < 0 { break } + if got <= 0 { + running = false + } else { + len = len + (got as uint) + let out: string = "" + let closeAfter: bool = false + let parsing: bool = true + while parsing { + let req = parseRequest(buf, len) + if !req.complete { + parsing = false + } else { + if req.isJson { + out = out + jsonResponse(ds, req.jcount, req.jmult, req.keepAlive) + } else if req.isBaseline { + out = out + baselineResponse(req.result, req.keepAlive) + } else { + out = out + pipelineResponse(req.keepAlive) + } + if !req.keepAlive { + closeAfter = true + } + // drop the consumed request, shift the leftover down + let rem: uint = len - req.consumed + foreach k: uint in 0u, rem { + buf[k] = buf[req.consumed + k] + } + len = rem + if closeAfter { + parsing = false + } + } + } + let ob = out.bytes() + if ob.length > 0u { + let w = tcp_write(fd, ob, ob.length) + if w < 0 { + running = false + } + } + if closeAfter { + running = false + } + if len >= BUFCAP { + running = false + } + } } - tcp_close(client_fd) + tcp_close(fd) } +// Accept loop — one of N SO_REUSEPORT shards. fn accept_loop(port: uint) -> void { let server = tcp_listen(port, 4096) - if server < 0 { return } - + if server < 0 { + return + } while true { let client = tcp_accept(server) if client >= 0 { @@ -43,7 +114,7 @@ fn accept_loop(port: uint) -> void { fn main() -> uint { let port: uint = 8080u - println("typev HttpArena server listening on :8080 (4 SO_REUSEPORT shards)") + println("typev HttpArena server listening on :8080 (pipelined + baseline)") foreach i in 0, 3, 1 { spawn accept_loop(port) diff --git a/frameworks/typev/bundle/benchmark-code/src/response.tc b/frameworks/typev/bundle/benchmark-code/src/response.tc new file mode 100644 index 000000000..8325dd5d2 --- /dev/null +++ b/frameworks/typev/bundle/benchmark-code/src/response.tc @@ -0,0 +1,23 @@ +// HTTP/1.1 response formatting for the typev HttpArena server. +// +// Pure formatters: given the parsed facts of a request they produce the wire +// response. No dependency on the parser module. + +// Pre-built response for the common case: a keep-alive /pipeline hit. +let local const PIPE_KA: string = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok" + +// /pipeline -> body "ok" +fn pipelineResponse(keepAlive: bool) -> string { + if keepAlive { + return PIPE_KA + } + return "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok" +} + +// /baseline11 -> body is the computed sum (text/plain) +fn baselineResponse(result: int, keepAlive: bool) -> string { + let conn: string = if keepAlive => "keep-alive" else "close" + let body: string = "" + result + let blen: int = body.bytes().length as int + return "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: " + blen + "\r\nConnection: " + conn + "\r\n\r\n" + body +} diff --git a/frameworks/typev/bundle/benchmark-code/stdlib/fs.tc b/frameworks/typev/bundle/benchmark-code/stdlib/fs.tc new file mode 100755 index 000000000..ad17b9938 --- /dev/null +++ b/frameworks/typev/bundle/benchmark-code/stdlib/fs.tc @@ -0,0 +1,509 @@ +/** + * Filesystem API — file I/O, metadata, and path utilities. + * + * Usage: + * from std.fs import File, readText, writeText, fs + * + * High-level functions (readText, writeText, readBytes, writeBytes, appendText) + * handle open/read/write/close in one call with error returns. + * + * The File class provides cursor-based sequential I/O for more control. + * + * The fs namespace wraps low-level metadata operations (exists, mkdir, etc.). + */ + +from std.internal.file import file +from std.internal.fs import fs as fs_api +from std.internal.io import io + +// ============================================================ +// File open flags (portable — same on all platforms) +// Combine with bitwise OR: WRONLY | CREATE | TRUNCATE +// ============================================================ + +let const RDONLY: int = 0x00 +let const WRONLY: int = 0x01 +let const RDWR: int = 0x02 +let const CREATE: int = 0x10 +let const TRUNCATE: int = 0x20 +let const APPEND: int = 0x40 +let const EXCL: int = 0x80 + +// ============================================================ +// High-level convenience functions +// ============================================================ + +/** + * Read an entire file as a UTF-8 string. + * @param path - file path + * Returns (content, 0) on success, ("", negative errno) on error. + */ +fn readText(path: string) -> (string, int) { + let fd = file.file_open(path, RDONLY, 0) + if fd < 0 { + return ("", fd) + } + let (size, stat_err) = file.file_stat(fd) + if stat_err != 0 { + file.file_close(fd) + return ("", stat_err) + } + + if size == 0 { + file.file_close(fd) + return ("", 0) + } + + let buf: uint[] = new uint[](size) + + let n = file.file_read(fd, buf, size, 0) + file.file_close(fd) + + if n < 0 { + return ("", n) + } + + let data = io.string_from_bytes(buf) + + return (data, 0) +} + +/** + * Write a UTF-8 string to a file (creates or truncates). + * @param path - file path + * @param content - string to write + * Returns 0 on success, negative errno on error. + */ +fn writeText(path: string, content: string) -> int { + let fd = file.file_open(path, WRONLY | CREATE | TRUNCATE, 0o644) + if fd < 0 { + return fd + } + + let data = content.bytes() + let n = file.file_write(fd, data, content.bytesLength(), 0) + file.file_close(fd) + + if n < 0 { + return n + } + + return 0 +} + +/** + * Append a UTF-8 string to a file (creates if needed). + * @param path - file path + * @param content - string to append + * Returns 0 on success, negative errno on error. + */ +fn appendText(path: string, content: string) -> int { + let fd = file.file_open(path, WRONLY | CREATE | APPEND, 0o644) + if fd < 0 { + return fd + } + + let (size, stat_err) = file.file_stat(fd) + let offset: uint = 0 + if stat_err == 0 && size > 0 { + offset = size + } + + let data = content.bytes() + let n = file.file_write(fd, data, content.bytesLength(), offset) + file.file_close(fd) + + if n < 0 { + return n + } + + return 0 +} + +/** + * Read an entire file as a byte array. + * @param path - file path + * Returns (data, 0) on success, ([], negative errno) on error. + */ +fn readBytes(path: string) -> (uint[], int) { + let fd = file.file_open(path, RDONLY, 0) + if fd < 0 { + return ([], fd) + } + + let (size, stat_err) = file.file_stat(fd) + if stat_err != 0 { + file.file_close(fd) + return ([], stat_err) + } + + if size == 0 { + file.file_close(fd) + return ([], 0) + } + + let buf: uint[] = [] + buf.resize(size) + + let n = file.file_read(fd, buf, size, 0) + file.file_close(fd) + + if n < 0 { + return ([], n) + } + + return (buf, 0) +} + +/** + * Write a byte array to a file (creates or truncates). + * @param path - file path + * @param data - bytes to write + * Returns 0 on success, negative errno on error. + */ +fn writeBytes(path: string, data: uint[]) -> int { + let fd = file.file_open(path, WRONLY | CREATE | TRUNCATE, 0o644) + if fd < 0 { + return fd + } + + let n = file.file_write(fd, data, data.length, 0) + file.file_close(fd) + + if n < 0 { + return n + } + + return 0 +} + +// ============================================================ +// File class — cursor-based sequential I/O +// ============================================================ + +/** + * A file handle with an internal cursor for sequential read/write. + * + * The cursor advances automatically after each read or write. + * Use seek() to reposition. Use the raw fd for direct pread/pwrite access. + * + * Example: + * let f = File.open("data.txt", RDONLY) + * let (line, n) = f.read(1024) + * f.close() + */ +type File = class { + let const fd: int + let cursor: uint + + /** + * Open a file. + * @param path - file path + * @param flags - open flags (RDONLY, WRONLY|CREATE|TRUNCATE, etc.) + * @param mode - Unix permission bits (default 0o644) + * Returns a File, or throws on error. + */ + fn init(path: string, flags: int, mode: int) { + let fd_ = file.file_open(path, flags, mode) + if fd_ < 0 { + throw "Failed to open file: " + path + } + this.{fd: fd_, cursor: 0} + } + + fn init(path: string, flags: int) { + let fd_ = file.file_open(path, flags, 0o644) + if fd_ < 0 { + throw "Failed to open file: " + path + } + this.{fd: fd_, cursor: 0} + } + + /** + * Read up to len bytes from the current cursor position. + * Advances the cursor by the number of bytes read. + * @param buf - buffer to read into + * @param len - max bytes to read + * Returns bytes read (0 = EOF, < 0 = error). + */ + fn read(buf: uint[], len: uint) -> int { + let n = file.file_read(this.fd, buf, len, this.cursor) + if n > 0 { + this.cursor = this.cursor + n as uint + } + return n + } + + /** + * Write bytes at the current cursor position. + * Advances the cursor by the number of bytes written. + * @param buf - buffer to write from + * @param len - bytes to write + * Returns bytes written, or < 0 on error. + */ + fn write(buf: uint[], len: uint) -> int { + let n = file.file_write(this.fd, buf, len, this.cursor) + if n > 0 { + this.cursor = this.cursor + n as uint + } + return n + } + + /** + * Write a string at the current cursor position. + * Advances the cursor by the number of bytes written. + */ + fn writeStr(s: string) -> int { + let data = s.bytes() + return this.write(data, s.bytesLength()) + } + + /** + * Seek to an absolute byte offset. + */ + fn seek(offset: uint) -> void { + this.cursor = offset + } + + /** + * Get the current cursor position. + */ + fn tell() -> uint { + return this.cursor + } + + /** + * Get the file size in bytes. + * Returns (size, error_code). On error: size=0, error_code=negative errno. + */ + fn size() -> (uint, int) { + return file.file_stat(this.fd) + } + + /** + * Flush file data to disk (fsync). + * Returns 0 on success, negative errno on error. + */ + fn fsync() -> int { + return file.file_fsync(this.fd) + } + + /** + * Close the file. + * Returns 0 on success, negative errno on error. + */ + fn close() -> int { + return file.file_close(this.fd) + } +} + +// ============================================================ +// Filesystem metadata — thin wrappers over internal/fs.tc +// ============================================================ + +namespace fs { + /** + * Check if a path exists (file or directory). + */ + fn exists(path: string) -> bool { + return fs_api.fs_exists(path) == 1 + } + + /** + * Check if a path is a directory. + */ + fn isDir(path: string) -> bool { + return fs_api.fs_is_dir(path) == 1 + } + + /** + * Check if a path is a regular file. + */ + fn isFile(path: string) -> bool { + return fs_api.fs_is_file(path) == 1 + } + + /** + * Delete a file. + * Returns 0 on success, negative errno on error. + */ + fn remove(path: string) -> int { + return fs_api.fs_unlink(path) + } + + /** + * Create a directory with default permissions (0o755). + * Returns 0 on success, negative errno on error. + */ + fn mkdir(path: string) -> int { + return fs_api.fs_mkdir(path, 0o755) + } + + /** + * Create a directory with specific permissions. + * Returns 0 on success, negative errno on error. + */ + fn mkdir(path: string, mode: int) -> int { + return fs_api.fs_mkdir(path, mode) + } + + /** + * Remove an empty directory. + * Returns 0 on success, negative errno on error. + */ + fn rmdir(path: string) -> int { + return fs_api.fs_rmdir(path) + } + + /** + * Rename or move a file/directory. + * Returns 0 on success, negative errno on error. + */ + fn rename(src: string, dest: string) -> int { + return fs_api.fs_rename(src, dest) + } + + /** + * Copy a file. Preserves permissions on Unix. + * Returns 0 on success, negative errno on error. + */ + fn copy(src: string, dest: string) -> int { + return fs_api.fs_copy(src, dest) + } + + /** + * List directory contents (excludes "." and ".."). + * When recursive is true, returns all entries under the directory tree + * with paths relative to the given path. + * Returns entries on success, empty array on error. + */ + fn listDir(path: string, recursive: bool = false) -> string[] { + let (entries, count) = fs_api.fs_listdir(path) + if count < 0 || !recursive { + return entries + } + + let result: string[] = [] + + foreach i in 0, count, 1 { + let entry = entries[i] + let fullPath = fs_api.fs_path_join(path, entry) + result = [...result, entry] + + if fs_api.fs_is_dir(fullPath) == 1 { + let subEntries = listDir(fullPath, true) + foreach j in 0, subEntries.length, 1 { + result = [...result, fs_api.fs_path_join(entry, subEntries[j])] + } + } + } + + return result + } + + /** + * Get file metadata. + * Returns (size_bytes, mode_bits, mtime_seconds). + * On error: size=-1. + */ + fn stat(path: string) -> (int, int, int) { + return fs_api.fs_stat_path(path) + } + + /** + * Create a symbolic link. + * @param target - what the link points to + * @param linkpath - where to create the link + * Returns 0 on success, negative errno on error. + */ + fn symlink(target: string, linkpath: string) -> int { + return fs_api.fs_symlink(target, linkpath) + } + + /** + * Read the target of a symbolic link. + * Returns the target path, or empty string on error. + */ + fn readlink(path: string) -> string { + return fs_api.fs_readlink(path) + } + + /** + * Resolve a path to its absolute, canonical form. + * Returns the resolved path, or empty string on error. + */ + fn realpath(path: string) -> string { + return fs_api.fs_realpath(path) + } + + /** + * Change file permissions. + * Returns 0 on success, negative errno on error. + */ + fn chmod(path: string, mode: int) -> int { + return fs_api.fs_chmod(path, mode) + } + + /** + * Get the current working directory. + * Returns the path, or empty string on error. + */ + fn cwd() -> string { + return fs_api.fs_getcwd() + } + + /** + * Change the current working directory. + * Returns 0 on success, negative errno on error. + */ + fn chdir(path: string) -> int { + return fs_api.fs_chdir(path) + } + + /** + * Get the platform path separator ('/' on Unix, '\\' on Windows). + */ + fn separator() -> uint { + return fs_api.fs_get_separator() + } + + /** + * Join two path components, normalizing separators and resolving . and .. + * Example: join("/usr", "./local/../bin") → "/usr/bin" + */ + fn join(a: string, b: string) -> string { + return fs_api.fs_path_join(a, b) + } + + /** + * Normalize a path: resolve . and .. segments, collapse double separators. + * Example: normalize("/usr/./local/../bin/") → "/usr/bin" + */ + fn normalize(path: string) -> string { + return fs_api.fs_path_normalize(path) + } + + /** + * Get the directory portion of a path. + * Example: dirname("/usr/bin/ls") → "/usr/bin" + */ + fn dirname(path: string) -> string { + return fs_api.fs_path_dirname(path) + } + + /** + * Get the filename portion of a path (last segment). + * Example: basename("/usr/bin/ls") → "ls" + */ + fn basename(path: string) -> string { + return fs_api.fs_path_basename(path) + } + + /** + * Get the file extension including the dot, or empty string if none. + * Example: extension("main.tc") → ".tc" + */ + fn extension(path: string) -> string { + return fs_api.fs_path_extension(path) + } +} diff --git a/frameworks/typev/bundle/benchmark-code/stdlib/internal/file.tc b/frameworks/typev/bundle/benchmark-code/stdlib/internal/file.tc new file mode 100644 index 000000000..1539b7852 --- /dev/null +++ b/frameworks/typev/bundle/benchmark-code/stdlib/internal/file.tc @@ -0,0 +1,71 @@ +/** + * Low-level async file I/O API. + * + * Uses fd-based pread/pwrite with explicit offsets. + * All read/write/fsync operations are async — the process suspends + * until the operation completes (other processes keep running). + * + * Portable open flags (combine with bitwise OR): + * RDONLY=0x00, WRONLY=0x01, RDWR=0x02, + * CREATE=0x10, TRUNCATE=0x20, APPEND=0x40, EXCL=0x80 + * + * Return conventions: + * - File descriptors: >= 0 on success, negative errno on error + * - Byte counts: > 0 on success, 0 = EOF, negative errno on error + * - Status codes: 0 on success, negative errno on error + */ +extern file from "stdfile" = { + + /** + * Open a file. + * @param path - file path + * @param flags - portable open flags (TV_FILE_* constants, combine with |) + * @param mode - Unix permission bits (e.g. 0o644), ignored on Windows + * Returns the file descriptor, or negative errno on error. + */ + fn file_open(path: string, flags: int, mode: int) -> int + + /** + * Read from a file at a specific offset (pread). + * Does not advance any internal cursor — the offset is explicit. + * @param fd - file descriptor + * @param buf - buffer to read into + * @param len - maximum bytes to read (clamped to buf.length) + * @param offset - byte offset in the file to read from + * Returns bytes read, 0 on EOF, or negative errno on error. + */ + fn file_read(fd: int, buf: uint[], len: uint, offset: uint) -> int + + /** + * Write to a file at a specific offset (pwrite). + * Does not advance any internal cursor — the offset is explicit. + * @param fd - file descriptor + * @param buf - buffer to write from + * @param len - bytes to write (clamped to buf.length) + * @param offset - byte offset in the file to write at + * Returns bytes written, or negative errno on error. + */ + fn file_write(fd: int, buf: uint[], len: uint, offset: uint) -> int + + /** + * Close a file descriptor. + * @param fd - file descriptor + * Returns 0 on success, negative errno on error. + */ + fn file_close(fd: int) -> int + + /** + * Flush file data to disk (fsync). + * @param fd - file descriptor + * Returns 0 on success, negative errno on error. + */ + fn file_fsync(fd: int) -> int + + /** + * Get the size of an open file (fstat). + * @param fd - file descriptor + * Returns (size_in_bytes, error_code). + * On success: error_code = 0. On error: size = 0, error_code = negative errno. + */ + fn file_stat(fd: int) -> (uint, int) +} diff --git a/frameworks/typev/bundle/benchmark-code/stdlib/internal/fs.tc b/frameworks/typev/bundle/benchmark-code/stdlib/internal/fs.tc new file mode 100755 index 000000000..10ecb6681 --- /dev/null +++ b/frameworks/typev/bundle/benchmark-code/stdlib/internal/fs.tc @@ -0,0 +1,149 @@ +/** + * Low-level filesystem metadata API. + * + * Path-based operations — no file handles, no async IO. + * For file read/write, use the stdfile API (file.tc). + * + * Return conventions: + * - Status codes: 0 on success, negative errno on error + * - Boolean queries: 1=yes, 0=no + * - String returns: empty string on error + */ +extern fs from "stdfs" = { + + /** + * Check if a path exists (file or directory). + * Returns 1 if exists, 0 if not. + */ + fn fs_exists(path: string) -> int + + /** + * Delete a file (not a directory — use fs_rmdir for that). + * Returns 0 on success, negative errno on error. + */ + fn fs_unlink(path: string) -> int + + /** + * Create a directory. + * @param path - directory path + * @param mode - Unix permission bits (e.g. 0o755), ignored on Windows + * Returns 0 on success, negative errno on error. + */ + fn fs_mkdir(path: string, mode: int) -> int + + /** + * Remove an empty directory. + * Returns 0 on success, negative errno on error (e.g. -ENOTEMPTY). + */ + fn fs_rmdir(path: string) -> int + + /** + * Rename/move a file or directory. + * Returns 0 on success, negative errno on error. + */ + fn fs_rename(src: string, dest: string) -> int + + /** + * Get file metadata by path. + * Returns (size_bytes, mode_bits, mtime_seconds). + * On error: size=-1, mode=0, mtime=0. + */ + fn fs_stat_path(path: string) -> (int, int, int) + + /** + * List directory contents (excludes "." and ".."). + * Returns (entries, count). + * On error: entries=empty array, count=negative errno. + */ + fn fs_listdir(path: string) -> (string[], int) + + /** + * Create a symbolic link. + * @param target - what the link points to + * @param linkpath - where to create the link + * Returns 0 on success, negative errno on error. + * Returns -ENOTSUP on Windows. + */ + fn fs_symlink(target: string, linkpath: string) -> int + + /** + * Read the target of a symbolic link. + * Returns the target path, or empty string on error. + */ + fn fs_readlink(path: string) -> string + + /** + * Resolve a path to its absolute, canonical form (resolves symlinks). + * Returns the resolved path, or empty string on error. + */ + fn fs_realpath(path: string) -> string + + /** + * Change file permissions. + * @param path - file path + * @param mode - Unix permission bits (e.g. 0o644) + * Returns 0 on success, negative errno on error. + * Returns -ENOTSUP on Windows. + */ + fn fs_chmod(path: string, mode: int) -> int + + /** + * Get the current working directory. + * Returns the path, or empty string on error. + */ + fn fs_getcwd() -> string + + /** + * Change the current working directory. + * Returns 0 on success, negative errno on error. + */ + fn fs_chdir(path: string) -> int + + /** + * Copy a file. Preserves permissions on Unix. + * Returns 0 on success, negative errno on error. + */ + fn fs_copy(src: string, dest: string) -> int + + /** + * Get the platform path separator ('/' on Unix, '\\' on Windows). + */ + fn fs_get_separator() -> uint + + /** + * Check if a path is a directory. + * Returns 1 if it is a directory, 0 otherwise. + */ + fn fs_is_dir(path: string) -> int + + /** + * Check if a path is a regular file. + * Returns 1 if it is a regular file, 0 otherwise. + */ + fn fs_is_file(path: string) -> int + + /** + * Join two path components, normalizing separators and ./.. segments. + */ + fn fs_path_join(a: string, b: string) -> string + + /** + * Normalize a path: resolve ./.. segments, collapse double separators. + */ + fn fs_path_normalize(path: string) -> string + + /** + * Get the directory portion of a path. + */ + fn fs_path_dirname(path: string) -> string + + /** + * Get the filename portion of a path (last segment). + */ + fn fs_path_basename(path: string) -> string + + /** + * Get the file extension (including the dot), or empty string if none. + */ + fn fs_path_extension(path: string) -> string +} diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index 3e53dcc17368bc06921cdfa892f20e05d1c2364d..6be866d832ed1d486a0f5dd1d705981fe63ed3c7 100644 GIT binary patch literal 13359 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 T_jpHbzk9qRDL$6sJ1zbn#;)YW delta 1812 zcmb7_%Wqpn6vofY+_~3}xOp_r%ZclzX&#XHCT-q*r3n=kMJ=SE1tJ`|j-w<^>|oy_ z!NwK~=z>MjlonNy*w96#iVBGZ8zdw)Y*3_#O$9=zVn;XZk-~Sz<`3vq{QYLm%sFQs zXOt)FpAIG#=C3U^648Q~h7v_7LN1NbF2%l0>{_cbU!7TgXV)!Mzej$ z{2J=BtvQo&2F&YD()=PihbDP&ie;K*hUFB?X_hlAXIajfhw7O1zi2AXH)7ii=}R#l zNpQx*^60ktM?5#rvO)Q%|w<~<_w)MOBxtVnXxqQ+5tS*~C-xlwhWb~|g7&#)& zn{Xr@A5UGqva)#Lt*aN_xU#sEDz9ftt7UU3k}&T_Ux|8%SPoX#Hn#HS@92}^0rZ0$ zEVF*On7f$|(m^hrE8Z#uE9*h9vl8S>+Z%;!P%M=<&7aK!!@-9;`H`JcaWkI_{2N&G z`gXQ-v+S>}ZTRdllEqH4K2OV;=S^K={l(of%^f1kZVNAx%`ca7{&sewz*}JQ>Gi#8 zp0*SedDCVl-h|(ztBbTC~ta zo=ZAIPL&kK6nqqXl%Rb_(5y+e^>~{0rJz%$*w&B9Y(H(=o^7AuITdB+aG7x_VY(Xu z1@Ur`#6c5R{YZs~W1kZaP4c*)NHZA5Fn`t;izB>@DAc6}+itPiWp&ubk?O4X zyGSV{R0;H}2U>N574&;}ysA|aJg8_@IRZzlM5VwGcodFduosMi$6$|vec&PRIM@jG zV;m==2EZ0@5Nrj9@I=`EB`^ZM404=dumK#wyf!e6I31)b%7>v7hEW(2;28Sd;0Z7Z zo&;0iIM@qL&;u++W4CyMLgO_^=QwF+iuRm-dW?rLiYLUcXpC#g5>Lny*J>&GePdkV cAB=6_1zGAk+wFu2aoz@)5OZ}d)H!eYA3(ec6#xJL diff --git a/frameworks/typev/meta.json b/frameworks/typev/meta.json index 45ae174b1..c8246896f 100644 --- a/frameworks/typev/meta.json +++ b/frameworks/typev/meta.json @@ -7,7 +7,10 @@ "repo": "https://github.com/praisethemoon/type-project", "enabled": true, "tests": [ - "pipelined" + "pipelined", + "baseline", + "limited-conn", + "json" ], "maintainers": ["praisethemoon"] } From 0cac88030bd5ad3eefbbd70e16015dda2c03514c Mon Sep 17 00:00:00 2001 From: praisethemoon Date: Sun, 17 May 2026 14:37:26 +0200 Subject: [PATCH 02/11] typev: buffer-based response path reducing allocations --- .../bundle/benchmark-code/src/dataset.tc | 86 ++++++++++++++---- .../typev/bundle/benchmark-code/src/http.tc | 26 ++++-- .../typev/bundle/benchmark-code/src/main.tc | 33 ++++--- .../bundle/benchmark-code/src/response.tc | 79 +++++++++++++--- frameworks/typev/bundle/output.tvbc | Bin 13359 -> 14672 bytes 5 files changed, 166 insertions(+), 58 deletions(-) diff --git a/frameworks/typev/bundle/benchmark-code/src/dataset.tc b/frameworks/typev/bundle/benchmark-code/src/dataset.tc index 9ed69ac0f..25a1eb8f0 100644 --- a/frameworks/typev/bundle/benchmark-code/src/dataset.tc +++ b/frameworks/typev/bundle/benchmark-code/src/dataset.tc @@ -1,23 +1,32 @@ -// Dataset loading + the JSON-processing profile response. +// Dataset loading + the JSON-processing profile writer. // // The json profile serves GET /json/{count}?m=M from a fixed 50-item dataset // (a JSON array). loadDataset() reads that file once and splits it into its -// item objects; jsonResponse() then emits the first `count` items per request -// with total = price * quantity * m spliced in — serialized fresh each call, -// no cached response. +// item objects; writeJson() then emits the first `count` items per request +// with total = price * quantity * m spliced in — written straight into the +// output buffer, no intermediate strings. from std.fs import readText +from response import writeBytes, writeInt, digitCount // One loaded dataset. prefixes[i] is item i's JSON text WITHOUT its closing -// '}', so a per-request `,"total":N}` can be appended. prices/quantities feed -// the total. n is the number of items loaded. +// '}', as raw bytes — so writeJson copies bytes with no allocation. prices / +// quantities feed the per-request total; n is the item count. type Dataset = struct { - prefixes: string[], + prefixes: byte[][], prices: int[], quantities: int[], n: int } +// JSON structural literals, byte-encoded once at module init. +let local const HDR_JSON: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ".bytes() +let local const J_CONN_KA: byte[] = "\r\nConnection: keep-alive\r\n\r\n".bytes() +let local const J_CONN_CL: byte[] = "\r\nConnection: close\r\n\r\n".bytes() +let local const J_OPEN: byte[] = "{\"items\":[".bytes() +let local const J_TOTAL: byte[] = ",\"total\":".bytes() +let local const J_COUNT: byte[] = "],\"count\":".bytes() + // Within data[start..end), find `key` and return the decimal integer that // follows it (after an optional run of spaces). 0 if not found. local fn fieldInt(data: byte[], start: uint, end: uint, key: byte[]) -> int { @@ -114,7 +123,8 @@ fn loadDataset(path: string) -> Dataset { } } let itemEnd: uint = p - ds.prefixes.push(txt.substring(itemStart, itemEnd - 1u)) + // prefix = item bytes minus the trailing '}' + ds.prefixes.push(data.slice(itemStart, itemEnd - 1u)) ds.prices.push(fieldInt(data, itemStart, itemEnd, priceKey)) ds.quantities.push(fieldInt(data, itemStart, itemEnd, qtyKey)) count = count + 1 @@ -123,20 +133,56 @@ fn loadDataset(path: string) -> Dataset { return ds } -// Build the json-profile response: first `count` dataset items, each with -// total = price * quantity * mult, plus the count field. -fn jsonResponse(ds: Dataset, count: int, mult: int, keepAlive: bool) -> string { - let limit: int = if count > ds.n => ds.n else count - let items: string = "" +// The dataset, loaded once at module initialization — shared by every +// connection handler, so there is no per-connection file read. The container +// mounts the dataset at /data/dataset.json. +let const DATASET_PATH: string = "/data/dataset.json" +let const DATASET: Dataset = loadDataset(DATASET_PATH) + +// /json/{count}?m=M -> {"items":[...first count items...],"count":N} +// total = price * quantity * mult. Written straight into dst. +fn writeJson(dst: byte[], pos: uint, count: int, mult: int, keepAlive: bool) -> uint { + let limit: int = if count > DATASET.n => DATASET.n else count + + // pass 1: body length, no allocation + let bodyLen: uint = J_OPEN.length + foreach i in 0, limit { + if i > 0 { + bodyLen = bodyLen + 1u + } + bodyLen = bodyLen + DATASET.prefixes[i as uint].length + bodyLen = bodyLen + J_TOTAL.length + bodyLen = bodyLen + digitCount(DATASET.prices[i as uint] * DATASET.quantities[i as uint] * mult) + bodyLen = bodyLen + 1u + } + bodyLen = bodyLen + J_COUNT.length + bodyLen = bodyLen + digitCount(limit) + bodyLen = bodyLen + 1u + + // headers + let p: uint = writeBytes(dst, pos, HDR_JSON) + p = writeInt(dst, p, bodyLen as int) + if keepAlive { + p = writeBytes(dst, p, J_CONN_KA) + } else { + p = writeBytes(dst, p, J_CONN_CL) + } + + // pass 2: body + p = writeBytes(dst, p, J_OPEN) foreach i in 0, limit { - let total: int = ds.prices[i as uint] * ds.quantities[i as uint] * mult if i > 0 { - items = items + "," + dst[p] = 44 as byte + p = p + 1u } - items = items + ds.prefixes[i as uint] + ",\"total\":" + total + "}" + p = writeBytes(dst, p, DATASET.prefixes[i as uint]) + p = writeBytes(dst, p, J_TOTAL) + p = writeInt(dst, p, DATASET.prices[i as uint] * DATASET.quantities[i as uint] * mult) + dst[p] = 125 as byte + p = p + 1u } - let body: string = "{\"items\":[" + items + "],\"count\":" + limit + "}" - let blen: int = body.bytes().length as int - let conn: string = if keepAlive => "keep-alive" else "close" - return "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " + blen + "\r\nConnection: " + conn + "\r\n\r\n" + body + p = writeBytes(dst, p, J_COUNT) + p = writeInt(dst, p, limit) + dst[p] = 125 as byte + return p + 1u } diff --git a/frameworks/typev/bundle/benchmark-code/src/http.tc b/frameworks/typev/bundle/benchmark-code/src/http.tc index aaa07cfcb..b155b0c95 100644 --- a/frameworks/typev/bundle/benchmark-code/src/http.tc +++ b/frameworks/typev/bundle/benchmark-code/src/http.tc @@ -28,6 +28,14 @@ type Chunk = struct { endPos: uint } +// Header-matching patterns, byte-encoded once at module init — avoids a +// per-call, per-request `.bytes()` allocation in the parser hot path. +let local const HDR_CONTENT_LENGTH: byte[] = "content-length".bytes() +let local const HDR_TRANSFER_ENCODING: byte[] = "transfer-encoding".bytes() +let local const HDR_CONNECTION: byte[] = "connection".bytes() +let local const VAL_CHUNKED: byte[] = "chunked".bytes() +let local const VAL_CLOSE: byte[] = "close".bytes() + // Parse a decimal integer from buf[start..end), ignoring non-digit bytes. local fn parseDigits(buf: byte[], start: uint, end: uint) -> int { let v: int = 0 @@ -44,8 +52,8 @@ local fn parseDigits(buf: byte[], start: uint, end: uint) -> int { // Case-insensitive match of `name` against the header name at buf[ls..), // requiring it to be immediately followed by ':'. -local fn matchHeader(buf: byte[], ls: uint, le: uint, name: string) -> bool { - let nb = name.bytes() +local fn matchHeader(buf: byte[], ls: uint, le: uint, name: byte[]) -> bool { + let const nb = name let nl: uint = nb.length if ls + nl >= le { return false @@ -73,8 +81,8 @@ local fn headerNumber(buf: byte[], ls: uint, le: uint) -> int { } // Case-insensitive substring search for `lit` within buf[start..end). -local fn containsCI(buf: byte[], start: uint, end: uint, lit: string) -> bool { - let lb = lit.bytes() +local fn containsCI(buf: byte[], start: uint, end: uint, lit: byte[]) -> bool { + let const lb = lit let ll: uint = lb.length if ll == 0u { return true @@ -219,14 +227,14 @@ fn parseRequest(buf: byte[], len: uint) -> Req { while le < he && (buf[le] as int) != 13 { le = le + 1u } - if matchHeader(buf, h, le, "content-length") { + if matchHeader(buf, h, le, HDR_CONTENT_LENGTH) { contentLen = headerNumber(buf, h, le) - } else if matchHeader(buf, h, le, "transfer-encoding") { - if containsCI(buf, h, le, "chunked") { + } else if matchHeader(buf, h, le, HDR_TRANSFER_ENCODING) { + if containsCI(buf, h, le, VAL_CHUNKED) { chunked = true } - } else if matchHeader(buf, h, le, "connection") { - if containsCI(buf, h, le, "close") { + } else if matchHeader(buf, h, le, HDR_CONNECTION) { + if containsCI(buf, h, le, VAL_CLOSE) { r.keepAlive = false } } diff --git a/frameworks/typev/bundle/benchmark-code/src/main.tc b/frameworks/typev/bundle/benchmark-code/src/main.tc index 9ab6d79a8..1cc643f49 100644 --- a/frameworks/typev/bundle/benchmark-code/src/main.tc +++ b/frameworks/typev/bundle/benchmark-code/src/main.tc @@ -8,14 +8,14 @@ from std.io import println from std.socket import tcp_listen, tcp_accept, tcp_read, tcp_write, tcp_close from http import parseRequest -from response import pipelineResponse, baselineResponse -from dataset import loadDataset, jsonResponse +from response import writePipeline, writeBaseline +from dataset import writeJson // Per-connection receive buffer capacity. let const BUFCAP: uint = 65536u -// Dataset for the json profile — the container mounts it at /data/dataset.json. -let const DATASET_PATH: string = "/data/dataset.json" +// Per-connection output buffer — responses are written into it, then flushed. +let const OBUFCAP: uint = 65536u // Serve one connection: read, parse every complete request in the buffer, // write the batched responses, compact the leftover, repeat. @@ -24,12 +24,11 @@ fn handle_client(fd: int) -> void { buf.resize(BUFCAP) let tmp: byte[] = [] tmp.resize(BUFCAP) + let obuf: byte[] = [] + obuf.resize(OBUFCAP) let len: uint = 0u let running: bool = true - // Dataset for the json profile — loaded once per connection. - let ds = loadDataset(DATASET_PATH) - while running { let got: int = 0 if len == 0u { @@ -49,7 +48,7 @@ fn handle_client(fd: int) -> void { running = false } else { len = len + (got as uint) - let out: string = "" + let opos: uint = 0u let closeAfter: bool = false let parsing: bool = true while parsing { @@ -57,12 +56,19 @@ fn handle_client(fd: int) -> void { if !req.complete { parsing = false } else { + if opos + 32768u > OBUFCAP { + let fw = tcp_write(fd, obuf, opos) + if fw < 0 { + running = false + } + opos = 0u + } if req.isJson { - out = out + jsonResponse(ds, req.jcount, req.jmult, req.keepAlive) + opos = writeJson(obuf, opos, req.jcount, req.jmult, req.keepAlive) } else if req.isBaseline { - out = out + baselineResponse(req.result, req.keepAlive) + opos = writeBaseline(obuf, opos, req.result, req.keepAlive) } else { - out = out + pipelineResponse(req.keepAlive) + opos = writePipeline(obuf, opos, req.keepAlive) } if !req.keepAlive { closeAfter = true @@ -78,9 +84,8 @@ fn handle_client(fd: int) -> void { } } } - let ob = out.bytes() - if ob.length > 0u { - let w = tcp_write(fd, ob, ob.length) + if opos > 0u { + let w = tcp_write(fd, obuf, opos) if w < 0 { running = false } diff --git a/frameworks/typev/bundle/benchmark-code/src/response.tc b/frameworks/typev/bundle/benchmark-code/src/response.tc index 8325dd5d2..feda39a8d 100644 --- a/frameworks/typev/bundle/benchmark-code/src/response.tc +++ b/frameworks/typev/bundle/benchmark-code/src/response.tc @@ -1,23 +1,72 @@ -// HTTP/1.1 response formatting for the typev HttpArena server. +// HTTP/1.1 response writers — append bytes into a caller-supplied buffer. // -// Pure formatters: given the parsed facts of a request they produce the wire -// response. No dependency on the parser module. +// No per-request string allocation: the fixed response chunks are byte-encoded +// once at module init, and integers are written as decimal digits straight +// into the output buffer. -// Pre-built response for the common case: a keep-alive /pipeline hit. -let local const PIPE_KA: string = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok" +// Fixed response chunks, byte-encoded once. +let local const PIPE_KA: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok".bytes() +let local const PIPE_CLOSE: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok".bytes() +let local const HDR_PLAIN: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ".bytes() +let local const CONN_KA: byte[] = "\r\nConnection: keep-alive\r\n\r\n".bytes() +let local const CONN_CLOSE: byte[] = "\r\nConnection: close\r\n\r\n".bytes() -// /pipeline -> body "ok" -fn pipelineResponse(keepAlive: bool) -> string { +// Copy src into dst starting at pos; return the position after the copy. +fn writeBytes(dst: byte[], pos: uint, src: byte[]) -> uint { + let n: uint = src.length + foreach i: uint in 0u, n { + dst[pos + i] = src[i] + } + return pos + n +} + +// Decimal digit count of a non-negative integer. +fn digitCount(n: int) -> uint { + if n <= 0 { + return 1u + } + let c: uint = 0u + let v: int = n + while v > 0 { + c = c + 1u + v = v / 10 + } + return c +} + +// Write n as decimal ASCII into dst at pos; return the position after. +fn writeInt(dst: byte[], pos: uint, n: int) -> uint { + if n <= 0 { + dst[pos] = 48 as byte + return pos + 1u + } + let end: uint = pos + digitCount(n) + let v: int = n + let p: uint = end + while v > 0 { + p = p - 1u + dst[p] = ((v % 10) + 48) as byte + v = v / 10 + } + return end +} + +// /pipeline -> fixed "ok" response. +fn writePipeline(dst: byte[], pos: uint, keepAlive: bool) -> uint { if keepAlive { - return PIPE_KA + return writeBytes(dst, pos, PIPE_KA) } - return "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok" + return writeBytes(dst, pos, PIPE_CLOSE) } -// /baseline11 -> body is the computed sum (text/plain) -fn baselineResponse(result: int, keepAlive: bool) -> string { - let conn: string = if keepAlive => "keep-alive" else "close" - let body: string = "" + result - let blen: int = body.bytes().length as int - return "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: " + blen + "\r\nConnection: " + conn + "\r\n\r\n" + body +// /baseline11 -> text/plain body = the decimal of `result`. +fn writeBaseline(dst: byte[], pos: uint, result: int, keepAlive: bool) -> uint { + let p: uint = writeBytes(dst, pos, HDR_PLAIN) + p = writeInt(dst, p, digitCount(result) as int) + if keepAlive { + p = writeBytes(dst, p, CONN_KA) + } else { + p = writeBytes(dst, p, CONN_CLOSE) + } + return writeInt(dst, p, result) } diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index 6be866d832ed1d486a0f5dd1d705981fe63ed3c7..ea9ed83d9d51d3ee54b16e79ec5af13d7473ab8f 100644 GIT binary patch delta 5638 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 From 0a6afa1fe15fc49787e277bad637814566c93152 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 May 2026 13:01:49 +0000 Subject: [PATCH 03/11] Benchmark results: typev --- site/data/baseline-4096.json | 20 ++++++++++++++++++++ site/data/baseline-512.json | 20 ++++++++++++++++++++ site/data/json-4096.json | 20 ++++++++++++++++++++ site/data/limited-conn-4096.json | 20 ++++++++++++++++++++ site/data/limited-conn-512.json | 20 ++++++++++++++++++++ site/data/pipelined-4096.json | 16 ++++++++-------- site/data/pipelined-512.json | 14 +++++++------- site/static/logs/baseline/4096/typev.log | 2 ++ site/static/logs/baseline/512/typev.log | 2 ++ site/static/logs/json/4096/typev.log | 2 ++ site/static/logs/limited-conn/4096/typev.log | 2 ++ site/static/logs/limited-conn/512/typev.log | 2 ++ site/static/logs/pipelined/4096/typev.log | 4 ++-- site/static/logs/pipelined/512/typev.log | 4 ++-- 14 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 site/static/logs/baseline/4096/typev.log create mode 100644 site/static/logs/baseline/512/typev.log create mode 100644 site/static/logs/json/4096/typev.log create mode 100644 site/static/logs/limited-conn/4096/typev.log create mode 100644 site/static/logs/limited-conn/512/typev.log diff --git a/site/data/baseline-4096.json b/site/data/baseline-4096.json index 4c10e622d..0edd04b17 100644 --- a/site/data/baseline-4096.json +++ b/site/data/baseline-4096.json @@ -1205,6 +1205,26 @@ "status_4xx": 0, "status_5xx": 0 }, + { + "framework": "typev", + "language": "Type-C", + "rps": 1704967, + "avg_latency": "246us", + "p99_latency": "2.48ms", + "cpu": "5643.1%", + "memory": "2.5GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "146.29MB/s", + "input_bw": "131.70MB/s", + "reconnects": 0, + "status_2xx": 8524839, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, { "framework": "userver", "language": "C++", diff --git a/site/data/baseline-512.json b/site/data/baseline-512.json index affa9887e..6caf4613c 100644 --- a/site/data/baseline-512.json +++ b/site/data/baseline-512.json @@ -1205,6 +1205,26 @@ "status_4xx": 0, "status_5xx": 0 }, + { + "framework": "typev", + "language": "Type-C", + "rps": 1719130, + "avg_latency": "100us", + "p99_latency": "395us", + "cpu": "5526.1%", + "memory": "2.5GiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "147.51MB/s", + "input_bw": "132.80MB/s", + "reconnects": 0, + "status_2xx": 8595650, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, { "framework": "userver", "language": "C++", diff --git a/site/data/json-4096.json b/site/data/json-4096.json index 965a53f47..fd49c7e9d 100644 --- a/site/data/json-4096.json +++ b/site/data/json-4096.json @@ -953,6 +953,26 @@ "status_4xx": 0, "status_5xx": 0 }, + { + "framework": "typev", + "language": "Type-C", + "rps": 46995, + "avg_latency": "23.89ms", + "p99_latency": "199.30ms", + "cpu": "885.0%", + "memory": "311MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "248.84MB/s", + "input_bw": "2.24MB/s", + "reconnects": 289176, + "status_2xx": 234978, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, { "framework": "uvicorn", "language": "Python", diff --git a/site/data/limited-conn-4096.json b/site/data/limited-conn-4096.json index b09654e84..5911f1156 100644 --- a/site/data/limited-conn-4096.json +++ b/site/data/limited-conn-4096.json @@ -1205,6 +1205,26 @@ "status_4xx": 0, "status_5xx": 0 }, + { + "framework": "typev", + "language": "Type-C", + "rps": 746836, + "avg_latency": "297us", + "p99_latency": "2.36ms", + "cpu": "3836.1%", + "memory": "367MiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "64.08MB/s", + "input_bw": "57.69MB/s", + "reconnects": 456812, + "status_2xx": 3734181, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, { "framework": "userver", "language": "C++", diff --git a/site/data/limited-conn-512.json b/site/data/limited-conn-512.json index 67cec0b22..c897e27a9 100644 --- a/site/data/limited-conn-512.json +++ b/site/data/limited-conn-512.json @@ -1205,6 +1205,26 @@ "status_4xx": 0, "status_5xx": 0 }, + { + "framework": "typev", + "language": "Type-C", + "rps": 1073551, + "avg_latency": "441us", + "p99_latency": "1.38ms", + "cpu": "5870.3%", + "memory": "220MiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "92.11MB/s", + "input_bw": "82.93MB/s", + "reconnects": 536904, + "status_2xx": 5367759, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, { "framework": "userver", "language": "C++", diff --git a/site/data/pipelined-4096.json b/site/data/pipelined-4096.json index 751e55896..2be37fd19 100644 --- a/site/data/pipelined-4096.json +++ b/site/data/pipelined-4096.json @@ -1145,18 +1145,18 @@ { "framework": "typev", "language": "Type-C", - "rps": 46023600, - "avg_latency": "199us", - "p99_latency": "1.18ms", - "cpu": "6164.8%", - "memory": "505MiB", + "rps": 2324368, + "avg_latency": "626us", + "p99_latency": "820us", + "cpu": "2588.8%", + "memory": "1.8GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "3.86GB/s", - "reconnects": 202612, - "status_2xx": 230118000, + "bandwidth": "199.44MB/s", + "reconnects": 262972, + "status_2xx": 11621840, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/pipelined-512.json b/site/data/pipelined-512.json index 16a758f98..9c608baf2 100644 --- a/site/data/pipelined-512.json +++ b/site/data/pipelined-512.json @@ -1145,18 +1145,18 @@ { "framework": "typev", "language": "Type-C", - "rps": 46259033, - "avg_latency": "140us", - "p99_latency": "743us", - "cpu": "6529.7%", - "memory": "315MiB", + "rps": 4592102, + "avg_latency": "421us", + "p99_latency": "3.70ms", + "cpu": "5099.2%", + "memory": "3.4GiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "3.87GB/s", + "bandwidth": "394.01MB/s", "reconnects": 0, - "status_2xx": 231295168, + "status_2xx": 22960512, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/baseline/4096/typev.log b/site/static/logs/baseline/4096/typev.log new file mode 100644 index 000000000..c0d584691 --- /dev/null +++ b/site/static/logs/baseline/4096/typev.log @@ -0,0 +1,2 @@ +typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) +typev HttpArena server listening on :8080 (pipelined + baseline) diff --git a/site/static/logs/baseline/512/typev.log b/site/static/logs/baseline/512/typev.log new file mode 100644 index 000000000..c0d584691 --- /dev/null +++ b/site/static/logs/baseline/512/typev.log @@ -0,0 +1,2 @@ +typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) +typev HttpArena server listening on :8080 (pipelined + baseline) diff --git a/site/static/logs/json/4096/typev.log b/site/static/logs/json/4096/typev.log new file mode 100644 index 000000000..c0d584691 --- /dev/null +++ b/site/static/logs/json/4096/typev.log @@ -0,0 +1,2 @@ +typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) +typev HttpArena server listening on :8080 (pipelined + baseline) diff --git a/site/static/logs/limited-conn/4096/typev.log b/site/static/logs/limited-conn/4096/typev.log new file mode 100644 index 000000000..c0d584691 --- /dev/null +++ b/site/static/logs/limited-conn/4096/typev.log @@ -0,0 +1,2 @@ +typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) +typev HttpArena server listening on :8080 (pipelined + baseline) diff --git a/site/static/logs/limited-conn/512/typev.log b/site/static/logs/limited-conn/512/typev.log new file mode 100644 index 000000000..c0d584691 --- /dev/null +++ b/site/static/logs/limited-conn/512/typev.log @@ -0,0 +1,2 @@ +typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) +typev HttpArena server listening on :8080 (pipelined + baseline) diff --git a/site/static/logs/pipelined/4096/typev.log b/site/static/logs/pipelined/4096/typev.log index 26cf90479..c0d584691 100644 --- a/site/static/logs/pipelined/4096/typev.log +++ b/site/static/logs/pipelined/4096/typev.log @@ -1,2 +1,2 @@ -typev: loaded '/app/output.tvbc' (v15, 11 functions, entry=10) -typev HttpArena server listening on :8080 (4 SO_REUSEPORT shards) +typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) +typev HttpArena server listening on :8080 (pipelined + baseline) diff --git a/site/static/logs/pipelined/512/typev.log b/site/static/logs/pipelined/512/typev.log index 26cf90479..c0d584691 100644 --- a/site/static/logs/pipelined/512/typev.log +++ b/site/static/logs/pipelined/512/typev.log @@ -1,2 +1,2 @@ -typev: loaded '/app/output.tvbc' (v15, 11 functions, entry=10) -typev HttpArena server listening on :8080 (4 SO_REUSEPORT shards) +typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) +typev HttpArena server listening on :8080 (pipelined + baseline) From 4310b5d033f2eca318f15b85263cb84e60ea9bbb Mon Sep 17 00:00:00 2001 From: praisethemoon Date: Sun, 17 May 2026 16:27:37 +0200 Subject: [PATCH 04/11] typev: restore pipeline fast path + allocation-free parsing --- .../bundle/benchmark-code/src/dataset.tc | 30 +-- .../typev/bundle/benchmark-code/src/http.tc | 105 +++++---- .../typev/bundle/benchmark-code/src/main.tc | 209 ++++++++++++------ .../bundle/benchmark-code/src/response.tc | 14 +- frameworks/typev/bundle/output.tvbc | Bin 14672 -> 16003 bytes 5 files changed, 217 insertions(+), 141 deletions(-) diff --git a/frameworks/typev/bundle/benchmark-code/src/dataset.tc b/frameworks/typev/bundle/benchmark-code/src/dataset.tc index 25a1eb8f0..52c76729d 100644 --- a/frameworks/typev/bundle/benchmark-code/src/dataset.tc +++ b/frameworks/typev/bundle/benchmark-code/src/dataset.tc @@ -1,17 +1,9 @@ -// Dataset loading + the JSON-processing profile writer. -// -// The json profile serves GET /json/{count}?m=M from a fixed 50-item dataset -// (a JSON array). loadDataset() reads that file once and splits it into its -// item objects; writeJson() then emits the first `count` items per request -// with total = price * quantity * m spliced in — written straight into the -// output buffer, no intermediate strings. +// Dataset loading + the json profile response writer. from std.fs import readText from response import writeBytes, writeInt, digitCount -// One loaded dataset. prefixes[i] is item i's JSON text WITHOUT its closing -// '}', as raw bytes — so writeJson copies bytes with no allocation. prices / -// quantities feed the per-request total; n is the item count. +// A loaded dataset; prefixes[i] is item i's JSON without its closing '}'. type Dataset = struct { prefixes: byte[][], prices: int[], @@ -19,7 +11,7 @@ type Dataset = struct { n: int } -// JSON structural literals, byte-encoded once at module init. +// JSON structural literals, byte-encoded once. let local const HDR_JSON: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ".bytes() let local const J_CONN_KA: byte[] = "\r\nConnection: keep-alive\r\n\r\n".bytes() let local const J_CONN_CL: byte[] = "\r\nConnection: close\r\n\r\n".bytes() @@ -27,8 +19,7 @@ let local const J_OPEN: byte[] = "{\"items\":[".bytes() let local const J_TOTAL: byte[] = ",\"total\":".bytes() let local const J_COUNT: byte[] = "],\"count\":".bytes() -// Within data[start..end), find `key` and return the decimal integer that -// follows it (after an optional run of spaces). 0 if not found. +// Decimal integer following `key` in data[start..end); 0 if not found. local fn fieldInt(data: byte[], start: uint, end: uint, key: byte[]) -> int { let kl: uint = key.length if kl == 0u || end < start + kl { @@ -96,7 +87,7 @@ fn loadDataset(path: string) -> Dataset { if p >= len || (data[p] as int) == 93 { break } - // data[p] is '{' — scan to the matching '}', string-aware + // scan to the matching '}', string-aware let itemStart: uint = p let depth: int = 0 let inStr: bool = false @@ -123,7 +114,7 @@ fn loadDataset(path: string) -> Dataset { } } let itemEnd: uint = p - // prefix = item bytes minus the trailing '}' + // prefix = item minus the trailing '}' ds.prefixes.push(data.slice(itemStart, itemEnd - 1u)) ds.prices.push(fieldInt(data, itemStart, itemEnd, priceKey)) ds.quantities.push(fieldInt(data, itemStart, itemEnd, qtyKey)) @@ -133,18 +124,15 @@ fn loadDataset(path: string) -> Dataset { return ds } -// The dataset, loaded once at module initialization — shared by every -// connection handler, so there is no per-connection file read. The container -// mounts the dataset at /data/dataset.json. +// Loaded once at module init; the container mounts it at /data/dataset.json. let const DATASET_PATH: string = "/data/dataset.json" let const DATASET: Dataset = loadDataset(DATASET_PATH) -// /json/{count}?m=M -> {"items":[...first count items...],"count":N} -// total = price * quantity * mult. Written straight into dst. +// /json/{count}?m=M -> {"items":[...],"count":N}; total = price*quantity*mult. fn writeJson(dst: byte[], pos: uint, count: int, mult: int, keepAlive: bool) -> uint { let limit: int = if count > DATASET.n => DATASET.n else count - // pass 1: body length, no allocation + // pass 1: body length let bodyLen: uint = J_OPEN.length foreach i in 0, limit { if i > 0 { diff --git a/frameworks/typev/bundle/benchmark-code/src/http.tc b/frameworks/typev/bundle/benchmark-code/src/http.tc index b155b0c95..6d9f30415 100644 --- a/frameworks/typev/bundle/benchmark-code/src/http.tc +++ b/frameworks/typev/bundle/benchmark-code/src/http.tc @@ -1,13 +1,7 @@ -// HTTP/1.1 request parsing for the typev HttpArena server. -// -// parseRequest() reads exactly one request from the front of an accumulating -// byte buffer. It returns complete=false when the request has not fully -// arrived yet (TCP fragmentation, or a pipelined batch still in flight), so -// the caller can recv() more bytes and retry. GET and POST are supported, -// with both Content-Length and chunked transfer-encoding bodies. +// HTTP/1.1 request parsing. parseRequest fills a caller-owned Req — created +// once per connection and reused — so parsing allocates nothing per request. -// A parsed request. `consumed` is the number of buffer bytes this request -// occupies, so the caller can drop them and parse the next one. +// A parsed request; `consumed` is its byte length in the buffer. type Req = struct { complete: bool, consumed: uint, @@ -28,8 +22,18 @@ type Chunk = struct { endPos: uint } -// Header-matching patterns, byte-encoded once at module init — avoids a -// per-call, per-request `.bytes()` allocation in the parser hot path. +// Allocate one Req / Chunk — a handler makes these once and reuses them. +fn newReq() -> Req { + let r: Req = {complete: false, consumed: 0u, isPipeline: false, isBaseline: false, isJson: false, isPost: false, keepAlive: true, result: 0, jcount: 0, jmult: 0} + return r +} + +fn newChunk() -> Chunk { + let c: Chunk = {ok: false, value: 0, endPos: 0u} + return c +} + +// Header-matching patterns, byte-encoded once at module init. let local const HDR_CONTENT_LENGTH: byte[] = "content-length".bytes() let local const HDR_TRANSFER_ENCODING: byte[] = "transfer-encoding".bytes() let local const HDR_CONNECTION: byte[] = "connection".bytes() @@ -50,8 +54,7 @@ local fn parseDigits(buf: byte[], start: uint, end: uint) -> int { return v } -// Case-insensitive match of `name` against the header name at buf[ls..), -// requiring it to be immediately followed by ':'. +// Case-insensitive match of `name` against the header name at buf[ls..), `:`-terminated. local fn matchHeader(buf: byte[], ls: uint, le: uint, name: byte[]) -> bool { let const nb = name let nl: uint = nb.length @@ -70,7 +73,7 @@ local fn matchHeader(buf: byte[], ls: uint, le: uint, name: byte[]) -> bool { return (buf[ls + nl] as int) == 58 } -// Integer value of a header line's value (the part after ':'). +// Integer value of a header line (the part after ':'). local fn headerNumber(buf: byte[], ls: uint, le: uint) -> int { let p: uint = ls while p < le && (buf[p] as int) != 58 { @@ -110,15 +113,16 @@ local fn containsCI(buf: byte[], start: uint, end: uint, lit: byte[]) -> bool { return false } -// Decode a chunked body starting at buf[start], summing the decimal digits of -// the decoded data. ok=false if the body has not fully arrived yet. -local fn decodeChunked(buf: byte[], start: uint, len: uint) -> Chunk { - let res: Chunk = {ok: false, value: 0, endPos: 0u} +// Decode a chunked body into `res` (reused); res.ok stays false if incomplete. +local fn decodeChunked(buf: byte[], start: uint, len: uint, mut res: Chunk) -> void { + res.ok = false + res.value = 0 + res.endPos = 0u let p: uint = start let acc: int = 0 while true { if p >= len { - return res + return } let size: int = 0 let any: bool = false @@ -140,27 +144,27 @@ local fn decodeChunked(buf: byte[], start: uint, len: uint) -> Chunk { p = p + 1u } if !any { - return res + return } if p + 1u >= len { - return res + return } if (buf[p] as int) != 13 || (buf[p + 1u] as int) != 10 { - return res + return } p = p + 2u if size == 0 { if p + 1u >= len { - return res + return } res.ok = true res.value = acc res.endPos = p + 2u - return res + return } let sz: uint = size as uint if p + sz + 2u > len { - return res + return } foreach q: uint in p, p + sz { let d: int = buf[q] as int @@ -170,14 +174,24 @@ local fn decodeChunked(buf: byte[], start: uint, len: uint) -> Chunk { } p = p + sz + 2u } - return res + return } -// Parse one HTTP request from the front of buf[0..len). -fn parseRequest(buf: byte[], len: uint) -> Req { - let r: Req = {complete: false, consumed: 0u, isPipeline: false, isBaseline: false, isJson: false, isPost: false, keepAlive: true, result: 0, jcount: 0, jmult: 0} +// Parse one HTTP request from buf[0..len) into the reused `r` (`c` is chunked- +// decode scratch); r.complete is left false if the request is incomplete. +fn parseRequest(buf: byte[], len: uint, mut r: Req, mut c: Chunk) -> void { + r.complete = false + r.consumed = 0u + r.isPipeline = false + r.isBaseline = false + r.isJson = false + r.isPost = false + r.keepAlive = true + r.result = 0 + r.jcount = 0 + r.jmult = 0 if len < 4u { - return r + return } // end of headers ("\r\n\r\n") @@ -189,7 +203,7 @@ fn parseRequest(buf: byte[], len: uint) -> Req { } } if headerEnd < 0 { - return r + return } let he: uint = headerEnd as uint let bodyStart: uint = he + 4u @@ -201,9 +215,8 @@ fn parseRequest(buf: byte[], len: uint) -> Req { } let pathStart: uint = sp + 1u - // method: POST is the 4-char method, GET the 3-char one — so the first - // space position equals the method length. Stored on the struct: a - // long-lived `let bool` holding a comparison miscompiles under -O. + // method length == first-space position: POST is 4, GET is 3. Stored on + // the struct — a long-lived `let bool` from a comparison miscompiles -O. r.isPost = sp == 4u // route on the character after '/': 'p'(112)=pipeline, 'b'(98)=baseline @@ -246,16 +259,16 @@ fn parseRequest(buf: byte[], len: uint) -> Req { let consumed: uint = bodyStart if r.isPost { if chunked { - let dec = decodeChunked(buf, bodyStart, len) - if !dec.ok { - return r + decodeChunked(buf, bodyStart, len, c) + if !c.ok { + return } - bodyVal = dec.value - consumed = dec.endPos + bodyVal = c.value + consumed = c.endPos } else { let cl: uint = if contentLen > 0 => (contentLen as uint) else 0u if len < bodyStart + cl { - return r + return } bodyVal = parseDigits(buf, bodyStart, bodyStart + cl) consumed = bodyStart + cl @@ -275,8 +288,8 @@ fn parseRequest(buf: byte[], len: uint) -> Req { let cnt: int = 0 let p: uint = q while p < lineEnd && cnt < 2 { - let c: int = buf[p] as int - if c >= 48 && c <= 57 { + let c2: int = buf[p] as int + if c2 >= 48 && c2 <= 57 { let v: int = 0 while p < lineEnd { let d: int = buf[p] as int @@ -296,12 +309,12 @@ fn parseRequest(buf: byte[], len: uint) -> Req { } if r.isJson { - // /json/{count}?m=M — count and m are the first two digit-runs of the target + // /json/{count}?m=M — count and m are the first two digit-runs let cnt: int = 0 let p: uint = pathStart while p < lineEnd && cnt < 2 { - let c: int = buf[p] as int - if c >= 48 && c <= 57 { + let c3: int = buf[p] as int + if c3 >= 48 && c3 <= 57 { let v: int = 0 while p < lineEnd { let d: int = buf[p] as int @@ -323,5 +336,5 @@ fn parseRequest(buf: byte[], len: uint) -> Req { } } - return r + return } diff --git a/frameworks/typev/bundle/benchmark-code/src/main.tc b/frameworks/typev/bundle/benchmark-code/src/main.tc index 1cc643f49..90355c53e 100644 --- a/frameworks/typev/bundle/benchmark-code/src/main.tc +++ b/frameworks/typev/bundle/benchmark-code/src/main.tc @@ -1,24 +1,62 @@ -// typev HttpArena server — HTTP/1.1, profiles: pipelined + baseline. +// typev HttpArena server — HTTP/1.1: pipelined, baseline, limited-conn, json. // -// A single incremental-parser server: each connection runs in its own -// lightweight process, accumulating bytes across recv() calls so it survives -// pipelined batches and TCP-fragmented requests. Parsing lives in http.tc, -// response formatting in response.tc. +// Each connection runs in its own process. /pipeline takes a zero-parse, +// zero-alloc fast path (count fixed-length requests, blast canned responses +// from a prebuilt batch); other routes use the http.tc parser with a +// per-connection Req/Chunk reused so parsing also allocates nothing. from std.io import println from std.socket import tcp_listen, tcp_accept, tcp_read, tcp_write, tcp_close -from http import parseRequest -from response import writePipeline, writeBaseline +from http import parseRequest, newReq, newChunk +from response import writePipeline, writeBaseline, PIPE_BATCH, PIPE_BATCH_N, RESP_LEN from dataset import writeJson -// Per-connection receive buffer capacity. +// Per-connection receive / output buffer capacities. let const BUFCAP: uint = 65536u - -// Per-connection output buffer — responses are written into it, then flushed. let const OBUFCAP: uint = 65536u -// Serve one connection: read, parse every complete request in the buffer, -// write the batched responses, compact the leftover, repeat. +// Write all `len` bytes of buf to fd, looping over partial sends. tcp_write +// has no offset arg, so a short send is followed by shifting the unsent tail +// to the front of buf and retrying. false on socket error. +fn writeAll(fd: int, buf: byte[], len: uint) -> bool { + let remaining: uint = len + while remaining > 0u { + let w = tcp_write(fd, buf, remaining) + if w <= 0 { + return false + } + let n: uint = w as uint + if n >= remaining { + return true + } + let rest: uint = remaining - n + foreach k: uint in 0u, rest { + buf[k] = buf[n + k] + } + remaining = rest + } + return true +} + +// Send `total` bytes straight out of the shared prebuilt PIPE_BATCH. A partial +// send is finished via `scratch` — PIPE_BATCH is a const and must not shift. +fn blastPipe(fd: int, scratch: byte[], total: uint) -> bool { + let w = tcp_write(fd, PIPE_BATCH, total) + if w < 0 { + return false + } + let sent: uint = w as uint + if sent >= total { + return true + } + let rest: uint = total - sent + foreach k: uint in 0u, rest { + scratch[k] = PIPE_BATCH[sent + k] + } + return writeAll(fd, scratch, rest) +} + +// Serve one connection. fn handle_client(fd: int) -> void { let buf: byte[] = [] buf.resize(BUFCAP) @@ -26,75 +64,110 @@ fn handle_client(fd: int) -> void { tmp.resize(BUFCAP) let obuf: byte[] = [] obuf.resize(OBUFCAP) + // Parse scratch — created once, reused for every request. + let req = newReq() + let chunk = newChunk() let len: uint = 0u let running: bool = true + // Pipeline fast path: armed on the first /pipeline request; reqLen is its + // byte length, and every later /pipeline request is byte-identical. + let pipeMode: bool = false + let reqLen: uint = 0u while running { - let got: int = 0 - if len == 0u { - got = tcp_read(fd, buf, BUFCAP) - } else { - // leftover present — read into a scratch buffer, then append - got = tcp_read(fd, tmp, BUFCAP - len) - if got > 0 { - let g: uint = got as uint - foreach k: uint in 0u, g { - buf[len + k] = tmp[k] + if pipeMode { + // FAST PATH — drain the socket, count whole requests, blast canned + // responses. `len` carries a request split across the recv edge. + let got: int = tcp_read(fd, buf, BUFCAP) + if got <= 0 { + running = false + } else { + let total: uint = len + (got as uint) + let count: uint = total / reqLen + len = total - count * reqLen + let remaining: uint = count + while remaining > 0u { + let n: uint = if remaining > PIPE_BATCH_N => PIPE_BATCH_N else remaining + if !blastPipe(fd, obuf, n * RESP_LEN) { + running = false + remaining = 0u + } else { + remaining = remaining - n + } } } - } - - if got <= 0 { - running = false } else { - len = len + (got as uint) - let opos: uint = 0u - let closeAfter: bool = false - let parsing: bool = true - while parsing { - let req = parseRequest(buf, len) - if !req.complete { - parsing = false - } else { - if opos + 32768u > OBUFCAP { - let fw = tcp_write(fd, obuf, opos) - if fw < 0 { - running = false - } - opos = 0u + // GENERAL PATH — incremental parse for baseline / json / + // limited-conn, and a /pipeline connection's first batch. + let got: int = 0 + if len == 0u { + got = tcp_read(fd, buf, BUFCAP) + } else { + // leftover present — read into scratch, then append + got = tcp_read(fd, tmp, BUFCAP - len) + if got > 0 { + let g: uint = got as uint + foreach k: uint in 0u, g { + buf[len + k] = tmp[k] } - if req.isJson { - opos = writeJson(obuf, opos, req.jcount, req.jmult, req.keepAlive) - } else if req.isBaseline { - opos = writeBaseline(obuf, opos, req.result, req.keepAlive) + } + } + + if got <= 0 { + running = false + } else { + len = len + (got as uint) + let opos: uint = 0u + let closeAfter: bool = false + let parsing: bool = true + while parsing { + parseRequest(buf, len, req, chunk) + if !req.complete { + parsing = false } else { - opos = writePipeline(obuf, opos, req.keepAlive) - } - if !req.keepAlive { - closeAfter = true - } - // drop the consumed request, shift the leftover down - let rem: uint = len - req.consumed - foreach k: uint in 0u, rem { - buf[k] = buf[req.consumed + k] + if opos + 32768u > OBUFCAP { + if !writeAll(fd, obuf, opos) { + running = false + } + opos = 0u + } + if req.isJson { + opos = writeJson(obuf, opos, req.jcount, req.jmult, req.keepAlive) + } else if req.isBaseline { + opos = writeBaseline(obuf, opos, req.result, req.keepAlive) + } else { + opos = writePipeline(obuf, opos, req.keepAlive) + } + // arm the fast path on the first /pipeline request + if !pipeMode && req.isPipeline && req.consumed > 0u { + pipeMode = true + reqLen = req.consumed + } + if !req.keepAlive { + closeAfter = true + } + // drop the consumed request, shift the leftover down + let rem: uint = len - req.consumed + foreach k: uint in 0u, rem { + buf[k] = buf[req.consumed + k] + } + len = rem + if closeAfter { + parsing = false + } } - len = rem - if closeAfter { - parsing = false + } + if opos > 0u { + if !writeAll(fd, obuf, opos) { + running = false } } - } - if opos > 0u { - let w = tcp_write(fd, obuf, opos) - if w < 0 { + if closeAfter { + running = false + } + if len >= BUFCAP { running = false } - } - if closeAfter { - running = false - } - if len >= BUFCAP { - running = false } } } @@ -119,7 +192,7 @@ fn accept_loop(port: uint) -> void { fn main() -> uint { let port: uint = 8080u - println("typev HttpArena server listening on :8080 (pipelined + baseline)") + println("typev HttpArena server listening on :8080") foreach i in 0, 3, 1 { spawn accept_loop(port) diff --git a/frameworks/typev/bundle/benchmark-code/src/response.tc b/frameworks/typev/bundle/benchmark-code/src/response.tc index feda39a8d..b536ff248 100644 --- a/frameworks/typev/bundle/benchmark-code/src/response.tc +++ b/frameworks/typev/bundle/benchmark-code/src/response.tc @@ -1,17 +1,19 @@ // HTTP/1.1 response writers — append bytes into a caller-supplied buffer. -// -// No per-request string allocation: the fixed response chunks are byte-encoded -// once at module init, and integers are written as decimal digits straight -// into the output buffer. // Fixed response chunks, byte-encoded once. -let local const PIPE_KA: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok".bytes() +let local const PIPE_STR: string = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok" +let local const PIPE_KA: byte[] = PIPE_STR.bytes() let local const PIPE_CLOSE: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok".bytes() + +// Pipeline fast path: PIPE_BATCH_N canned responses concatenated, prebuilt once. +let const RESP_LEN: uint = PIPE_KA.length +let const PIPE_BATCH_N: uint = 512u +let const PIPE_BATCH: byte[] = PIPE_STR.repeat(PIPE_BATCH_N).bytes() let local const HDR_PLAIN: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ".bytes() let local const CONN_KA: byte[] = "\r\nConnection: keep-alive\r\n\r\n".bytes() let local const CONN_CLOSE: byte[] = "\r\nConnection: close\r\n\r\n".bytes() -// Copy src into dst starting at pos; return the position after the copy. +// Copy src into dst at pos; return the position after. fn writeBytes(dst: byte[], pos: uint, src: byte[]) -> uint { let n: uint = src.length foreach i: uint in 0u, n { diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index ea9ed83d9d51d3ee54b16e79ec5af13d7473ab8f..4beed3d527e126832439e7f3f4c00e77c7665b0d 100644 GIT binary patch delta 5226 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 delta 4329 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 From 097311d579217b31eb5c5e6d42102f2716033fbb Mon Sep 17 00:00:00 2001 From: praisethemoon Date: Sun, 17 May 2026 19:02:45 +0200 Subject: [PATCH 05/11] typev: update compiler & bytecode (codegen bug), removed fixed file opt --- frameworks/typev/Dockerfile | 2 +- .../typev/bundle/benchmark-code/src/main.tc | 12 ++++++------ frameworks/typev/bundle/output.tvbc | Bin 16003 -> 16009 bytes 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frameworks/typev/Dockerfile b/frameworks/typev/Dockerfile index 2f7cb5c2b..1b1ea558b 100644 --- a/frameworks/typev/Dockerfile +++ b/frameworks/typev/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /app # typev VM — binary + FFI plugins (json, stdcore, stdsocket, ...). Pre-built # (-O3 -march=x86-64-v3, static liburing) and fetched from object storage RUN wget -q -O /tmp/typev.zip \ - https://typev.s3.fr-par.scw.cloud/typev-16-05-2026.zip && \ + https://typev.s3.fr-par.scw.cloud/typev-17-05-2026.zip && \ unzip -q /tmp/typev.zip -d /app && \ rm /tmp/typev.zip diff --git a/frameworks/typev/bundle/benchmark-code/src/main.tc b/frameworks/typev/bundle/benchmark-code/src/main.tc index 90355c53e..d36587c72 100644 --- a/frameworks/typev/bundle/benchmark-code/src/main.tc +++ b/frameworks/typev/bundle/benchmark-code/src/main.tc @@ -15,6 +15,8 @@ from dataset import writeJson let const BUFCAP: uint = 65536u let const OBUFCAP: uint = 65536u +let const PORT: uint = 8080u + // Write all `len` bytes of buf to fd, looping over partial sends. tcp_write // has no offset arg, so a short send is followed by shifting the unsent tail // to the front of buf and retrying. false on socket error. @@ -176,8 +178,8 @@ fn handle_client(fd: int) -> void { } // Accept loop — one of N SO_REUSEPORT shards. -fn accept_loop(port: uint) -> void { - let server = tcp_listen(port, 4096) +fn accept_loop() -> void { + let server = tcp_listen(PORT, 4096) if server < 0 { return } @@ -190,15 +192,13 @@ fn accept_loop(port: uint) -> void { } fn main() -> uint { - let port: uint = 8080u - println("typev HttpArena server listening on :8080") foreach i in 0, 3, 1 { - spawn accept_loop(port) + spawn accept_loop() } - let server = tcp_listen(port, 4096) + let server = tcp_listen(PORT, 4096) if server < 0 { println("Failed to start server") return 1u diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index 4beed3d527e126832439e7f3f4c00e77c7665b0d..90df5ebf979429a588c9e2b44b46d17c689e58db 100644 GIT binary patch delta 1067 zcmYk5+e=hY6vn@On=|8F9B(5eVMbYYv(ZK{3ZkAIHM2AbbDrwO$c%0j$vG)Sc8Bv2 z489lLNXLAOh~9b`=pX2xsGvaTJBt%EhrPb<{MI__?7h#P#rdWA^s%F-CmI2qSCcF0 za-rSBzsumit2R^kx~XRfa|%WoU{>oWtF0w7u*wRvuw+gm20K`?}|5Vr9mGGF3tJzU5sdmLK8chCGGvS?qmaYt3-6Kvg z%c3;9$a;*2HG{;useKq2*QqZuA*6C)<6z03k-H0w78k|OGAhi8zbyQU_^ZONi@zcK zrubXJZ;KBYEf&NtGAi5^e^2;*@k_!VihtBBi6?UNR1(j_kAv;S7xYQIlf*&9EbCEpiupD)WX(Hkcknb;pckcT!!I5QM$nVEk! zVUKa^@@3%Su-DF&M_ZnE-;^9KE!;I zxxzewn%Rtc+GwXOYI+N`HqJQmtuYVC`(qxGZ;N??d?4n@hzr|!U>ht4UwqTjJK%JR zMLXl7G;4Onn=|CQW1b}+jCnixo|xzOl@;Om@%_yu)a(%F9k3krv}L26+&fG?Ypk>D zxX_794%WF-P<-8;o8p9n8~6&^`nWT6>zwOC&F$qzdNtiV#5?yqPwt(2-h);!g113+ H?cl;6`mBT! From e0ec5c3123167649ef83971672fc349555564f06 Mon Sep 17 00:00:00 2001 From: praisethemoon Date: Sun, 17 May 2026 20:16:38 +0200 Subject: [PATCH 06/11] type: json micro opt: compact json --- .../bundle/benchmark-code/src/dataset.tc | 39 +++++++++++++++++- frameworks/typev/bundle/output.tvbc | Bin 16009 -> 16512 bytes 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/frameworks/typev/bundle/benchmark-code/src/dataset.tc b/frameworks/typev/bundle/benchmark-code/src/dataset.tc index 52c76729d..a2ce29d17 100644 --- a/frameworks/typev/bundle/benchmark-code/src/dataset.tc +++ b/frameworks/typev/bundle/benchmark-code/src/dataset.tc @@ -54,6 +54,41 @@ local fn fieldInt(data: byte[], start: uint, end: uint, key: byte[]) -> int { return 0 } +// Strip insignificant (out-of-string) whitespace from data[start..end) and +// return the compacted bytes. +local fn compactItem(data: byte[], start: uint, end: uint) -> byte[] { + let out: byte[] = new byte[](end - start) + let w: uint = 0u + let inStr: bool = false + let escaped: bool = false + foreach p: uint in start, end { + let c: int = data[p] as int + if inStr { + out[w] = data[p] + w = w + 1u + if escaped { + // this byte was escaped by a preceding backslash — consume it + escaped = false + } else if c == 92 { + escaped = true + } else if c == 34 { + inStr = false + } + } else { + if c == 34 { + inStr = true + out[w] = data[p] + w = w + 1u + } else if c != 32 && c != 10 && c != 13 && c != 9 { + out[w] = data[p] + w = w + 1u + } + } + } + out.resize(w) + return out +} + // Read the dataset file and split the top-level JSON array into its objects. fn loadDataset(path: string) -> Dataset { let ds: Dataset = {prefixes: [], prices: [], quantities: [], n: 0} @@ -114,8 +149,8 @@ fn loadDataset(path: string) -> Dataset { } } let itemEnd: uint = p - // prefix = item minus the trailing '}' - ds.prefixes.push(data.slice(itemStart, itemEnd - 1u)) + // prefix = item minus the trailing '}', whitespace-compacted + ds.prefixes.push(compactItem(data, itemStart, itemEnd - 1u)) ds.prices.push(fieldInt(data, itemStart, itemEnd, priceKey)) ds.quantities.push(fieldInt(data, itemStart, itemEnd, qtyKey)) count = count + 1 diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index 90df5ebf979429a588c9e2b44b46d17c689e58db..dbfce871946e5b17dd5a027039c98a47fca23f11 100644 GIT binary patch delta 1225 zcmZWp%WD&15TDuiCc9f}Op~-VX;VuJVyi-u+KOU5wRo}i;iFx(Y4spJ&|Ip>u22+D z9zIaPqY7SSMe$OKAoYP11U-4szrnls`@V=)U6`3~=k?9ZZ+35Ak8g11;OO*Z3lW{+ zDbgW}wDoDG>Zj`$&tE!SZ;Uk-&nI8Kg&-n22D8r8l7ZBS!I@^j9ikmNYZnYnM+$>2 z7;F&qY{JcjUkVXvGH2aGal>?pJTgo038|pouj;1N?<3M8zl6Au^q75>+Yf0U&e1I8 zJi-eMGO0smNJBW6r5@)>Gr*QUyW@~{fp&rhK?ATUKiI68Z6;a7#!Ex$g-@3%xE=Sf ziQ-W}2{I%z=EzJD2TU-U9H26v0x3&yQVzcVS1<+eLvRT`${t`0zKRc2XC^vT$?g9p z5R!(X8AS1%`z*SolNEi`IX;YxALQJDLJo2hNIn`!Cu9T1^uT3>CT#(?D6e<{yp1{( z?*yNw7mB|G|7@(N^Qz>=Yjs|eTqA&_Qi``72RI|ro%i`(@~AV-(PRu{9ZsPfI5`TC zTHNp)#%#PMx-Bt#01odGxPA{i;mC6ldH|yI9kH6HO+iR`^qd2=fwIsmALs$P2heR9 zD|23>T=XYt3hM=f3&ZtaNv(LmM`@|%#%vsDzr#*XK(<2nH#lel7vxd^)kDX!0R`;4 zs2TePaxEIL(Le_4yoKC3%96dFGHBi=ZP~_EzYNejn6$C(n>3$dZ5c)UW3!aa)wE@o zd`u;cZvzdSjTX|n_EF<_Li{LRcH2>j1h0@Rm9}eqL_F@k!jqy<9^ol5Rldp7;!F8m z`wZ4Wy5ZY&}$fL2`j)NRSlzzgv7l z_=8V)Ry-dp0_#tMcX&=L?tIWIw?^&^utq0%Vxr9ua=#3a99cT)-PL1oD8?$?oE7IP zC;6m!Um0!9!YKbNdr^^G)HYG9KIBv4Rdt+C3%@qO^Ws!3&jqnmJIi(Pt=7qT@uN1v i3u1Vvs-_^9H!qfkZt-Hmd%Bo&qCDKkZZbZ+-}niX@}>y@ delta 805 zcmY*XOKTHh6g_t`$;>2~ByE~Z+9qjg8&eeuQD~a9V6Co-o7OhI>QvK;;Hp%2QZhnN zTy=3p1jUUTH!jNfK%|=@C`Awy#Qz}`!HxIJ6vTluXYOO}&9D#Td2cJyG&A!VzQz8S!^>#2=D;QaSRk zs(A-aqMnm9|##)J1c8A(7)I^S_WwyLq_{Q7U{+;P3u^&Gh+#g zA%=vDo9(q&Nc*ahH_!W}4NKjOBgpyd!^bpQ;HM+E=p3g;r)ZJSj^3js{x-TtO@3C+ zQJmkFSLwVzcvvI4;1?^8G`h&IkIeO`C6q4INh$@{e(LDY2wh^m`hqU=%W9E&`EzxP zmU;R3lk$M1C^1-qUI9FyM<^}O1u;R?&z~64=?c%(hUMm4wH0!Cw>I0Oq@^S}EjOYh znBmOib6VkdlXG;H_4>lVHIb@XEJU!>G$hBR-pKKK{RXx8M?Fst@7Jejm8*@3?kOm# ZI=tSvPiy|~Mu{lP{nHgP{ZrE?^}lpqdkz2q From 6116a70bc6a0aa62e41589b993e62955baaa678c Mon Sep 17 00:00:00 2001 From: praisethemoon Date: Sun, 17 May 2026 22:18:30 +0200 Subject: [PATCH 07/11] typev: inplace copy instead of manual iterate and push --- .../bundle/benchmark-code/src/response.tc | 12 +++++++----- frameworks/typev/bundle/output.tvbc | Bin 16512 -> 16501 bytes 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frameworks/typev/bundle/benchmark-code/src/response.tc b/frameworks/typev/bundle/benchmark-code/src/response.tc index b536ff248..19a9601f7 100644 --- a/frameworks/typev/bundle/benchmark-code/src/response.tc +++ b/frameworks/typev/bundle/benchmark-code/src/response.tc @@ -13,13 +13,15 @@ let local const HDR_PLAIN: byte[] = "HTTP/1.1 200 OK\r\nContent-Type: text/plain let local const CONN_KA: byte[] = "\r\nConnection: keep-alive\r\n\r\n".bytes() let local const CONN_CLOSE: byte[] = "\r\nConnection: close\r\n\r\n".bytes() -// Copy src into dst at pos; return the position after. +// Copy src into dst at pos; return the position after. The copy is a single +// native blit (copyFrom); dst is grown first if it can't hold the span. fn writeBytes(dst: byte[], pos: uint, src: byte[]) -> uint { - let n: uint = src.length - foreach i: uint in 0u, n { - dst[pos + i] = src[i] + let need: uint = pos + src.length + if need > dst.length { + dst.resize(need) } - return pos + n + dst.copyFrom(pos, src) + return need } // Decimal digit count of a non-negative integer. diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index dbfce871946e5b17dd5a027039c98a47fca23f11..768b58e93fbadec2a597f818bfe92b9fdf710ef6 100644 GIT binary patch delta 73 zcmZo@Wc=E|xM7cl6&C{o12ZE7J2#MKWMBi5tPJ_g3{2`QKv8A}mV8zQ1`!qp);I>H Xh613{I0lA>Vg^R0OU#=YE$i6 Date: Sun, 17 May 2026 22:47:14 +0200 Subject: [PATCH 08/11] typev: 4 -> 32 shards --- .../typev/bundle/benchmark-code/src/main.tc | 2 +- frameworks/typev/bundle/output.tvbc | Bin 16501 -> 16501 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/frameworks/typev/bundle/benchmark-code/src/main.tc b/frameworks/typev/bundle/benchmark-code/src/main.tc index d36587c72..9f9bcb9af 100644 --- a/frameworks/typev/bundle/benchmark-code/src/main.tc +++ b/frameworks/typev/bundle/benchmark-code/src/main.tc @@ -194,7 +194,7 @@ fn accept_loop() -> void { fn main() -> uint { println("typev HttpArena server listening on :8080") - foreach i in 0, 3, 1 { + foreach i in 0, 31, 1 { spawn accept_loop() } diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index 768b58e93fbadec2a597f818bfe92b9fdf710ef6..ed59921b93715fcd6ff0f0fdccba6abb4530f821 100644 GIT binary patch delta 771 zcmYk4&1(}u7{;HOc_)cAyGd(JK`X7G*80(^X=qVFZ(_AJNrRU{K=539R1~vH{h$Y_ ztSBzI3Zfo_peTw3!Gov#33~Tj!JE&%?m?EB=a=8U^X_JLwj#3?nYZ67BM$#xd1P1b zXJuh@vW`^)XB@C%ZCYnV=p4j|I)TvnMtr$CZR+v;>f+uOh699cxJdFXAVj_m6EOq! z4yCvh|E@0YsRO$J?1sT8^8J91C?RO05=l^xS7tU%qc>IiVc0aBy<(d2>g-oD7FXvc zOtZH<*R^(_6#qF{>s)6S;08wlHOXQG$0y~_>c1#&roQJ-uh~wz z+uografUh1w9E_4GV>yHoVma}%e=&ln2Si^_Fz6e5KKyalvjwp&EOyeW? zLWN!kO}kcn5t$z#xN|i>LHDjBxbyU-1!?M@)8D;y3aWcOHtVrj`(7O{;osx%-_!qD zZB0x^=o&cVfR43!*NTuih!JH1A@hx7xi)K}WVN=myM?g;VH+-%QVR%CYQx0TfG34g zUQB-1R(3?dHUQgUFoDt_;1MGT+Nj17L`i4vt7-IS>OTxa!}*J*nRMsB8JE-+rcATH zvXEIj943E`)q9tj0$kw|Tvgv;HR!6puKb4jo62vgzpea^`n$^SsgGF&_tigOHF%_c zRrzD}Pn17X|NMeBUg*n9ZM;^WvJN3r|AtlYR>?c{@0EXGt>BaT&&t=-f5~~@AAe|j zsojIDY~dM@Vv-)#TaVQ4E%*lVse+ft_Z57Ie7fMn^H7JS<$+5X)Kda%l{y%yFYIWGh4R8^{|~^85yJJ`Upn?@v(; jb&1Lw^QWm5>KW=T>RIYZ>N#pmJ&%E} Date: Sun, 17 May 2026 20:56:03 +0000 Subject: [PATCH 09/11] Benchmark results: typev --- site/data/baseline-4096.json | 16 ++++++++-------- site/data/baseline-512.json | 16 ++++++++-------- site/data/json-4096.json | 18 +++++++++--------- site/data/limited-conn-4096.json | 18 +++++++++--------- site/data/limited-conn-512.json | 18 +++++++++--------- site/data/pipelined-4096.json | 16 ++++++++-------- site/data/pipelined-512.json | 14 +++++++------- site/static/logs/baseline/4096/typev.log | 5 +++-- site/static/logs/baseline/512/typev.log | 5 +++-- site/static/logs/json/4096/typev.log | 5 +++-- site/static/logs/limited-conn/4096/typev.log | 5 +++-- site/static/logs/limited-conn/512/typev.log | 5 +++-- site/static/logs/pipelined/4096/typev.log | 5 +++-- site/static/logs/pipelined/512/typev.log | 5 +++-- 14 files changed, 79 insertions(+), 72 deletions(-) diff --git a/site/data/baseline-4096.json b/site/data/baseline-4096.json index 0edd04b17..9e252e2a2 100644 --- a/site/data/baseline-4096.json +++ b/site/data/baseline-4096.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 1704967, - "avg_latency": "246us", - "p99_latency": "2.48ms", - "cpu": "5643.1%", - "memory": "2.5GiB", + "rps": 1916753, + "avg_latency": "243us", + "p99_latency": "1.11ms", + "cpu": "6439.9%", + "memory": "292MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "146.29MB/s", - "input_bw": "131.70MB/s", + "bandwidth": "164.46MB/s", + "input_bw": "148.06MB/s", "reconnects": 0, - "status_2xx": 8524839, + "status_2xx": 9583767, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/baseline-512.json b/site/data/baseline-512.json index 6caf4613c..5cd57b6fd 100644 --- a/site/data/baseline-512.json +++ b/site/data/baseline-512.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 1719130, - "avg_latency": "100us", - "p99_latency": "395us", - "cpu": "5526.1%", - "memory": "2.5GiB", + "rps": 1865595, + "avg_latency": "134us", + "p99_latency": "795us", + "cpu": "6472.4%", + "memory": "275MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "147.51MB/s", - "input_bw": "132.80MB/s", + "bandwidth": "160.08MB/s", + "input_bw": "144.11MB/s", "reconnects": 0, - "status_2xx": 8595650, + "status_2xx": 9327978, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/json-4096.json b/site/data/json-4096.json index fd49c7e9d..f948a7092 100644 --- a/site/data/json-4096.json +++ b/site/data/json-4096.json @@ -956,19 +956,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 46995, - "avg_latency": "23.89ms", - "p99_latency": "199.30ms", - "cpu": "885.0%", - "memory": "311MiB", + "rps": 544745, + "avg_latency": "5.58ms", + "p99_latency": "11.40ms", + "cpu": "3102.4%", + "memory": "442MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "248.84MB/s", - "input_bw": "2.24MB/s", - "reconnects": 289176, - "status_2xx": 234978, + "bandwidth": "1.84GB/s", + "input_bw": "25.98MB/s", + "reconnects": 108821, + "status_2xx": 2723726, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/limited-conn-4096.json b/site/data/limited-conn-4096.json index 5911f1156..085e69c99 100644 --- a/site/data/limited-conn-4096.json +++ b/site/data/limited-conn-4096.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 746836, - "avg_latency": "297us", - "p99_latency": "2.36ms", - "cpu": "3836.1%", - "memory": "367MiB", + "rps": 663145, + "avg_latency": "5.67ms", + "p99_latency": "104.30ms", + "cpu": "3258.4%", + "memory": "408MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "64.08MB/s", - "input_bw": "57.69MB/s", - "reconnects": 456812, - "status_2xx": 3734181, + "bandwidth": "56.90MB/s", + "input_bw": "51.23MB/s", + "reconnects": 331619, + "status_2xx": 3315725, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/limited-conn-512.json b/site/data/limited-conn-512.json index c897e27a9..c90d315f9 100644 --- a/site/data/limited-conn-512.json +++ b/site/data/limited-conn-512.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 1073551, - "avg_latency": "441us", - "p99_latency": "1.38ms", - "cpu": "5870.3%", - "memory": "220MiB", + "rps": 288546, + "avg_latency": "1.75ms", + "p99_latency": "72.60ms", + "cpu": "1386.9%", + "memory": "178MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "92.11MB/s", - "input_bw": "82.93MB/s", - "reconnects": 536904, - "status_2xx": 5367759, + "bandwidth": "24.76MB/s", + "input_bw": "22.29MB/s", + "reconnects": 144278, + "status_2xx": 1442730, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/pipelined-4096.json b/site/data/pipelined-4096.json index 2be37fd19..a60c039a2 100644 --- a/site/data/pipelined-4096.json +++ b/site/data/pipelined-4096.json @@ -1145,18 +1145,18 @@ { "framework": "typev", "language": "Type-C", - "rps": 2324368, - "avg_latency": "626us", - "p99_latency": "820us", - "cpu": "2588.8%", - "memory": "1.8GiB", + "rps": 47909920, + "avg_latency": "247us", + "p99_latency": "1.15ms", + "cpu": "6198.4%", + "memory": "555MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "199.44MB/s", - "reconnects": 262972, - "status_2xx": 11621840, + "bandwidth": "4.01GB/s", + "reconnects": 0, + "status_2xx": 239549600, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/pipelined-512.json b/site/data/pipelined-512.json index 9c608baf2..3113b2aec 100644 --- a/site/data/pipelined-512.json +++ b/site/data/pipelined-512.json @@ -1145,18 +1145,18 @@ { "framework": "typev", "language": "Type-C", - "rps": 4592102, - "avg_latency": "421us", - "p99_latency": "3.70ms", - "cpu": "5099.2%", - "memory": "3.4GiB", + "rps": 43128227, + "avg_latency": "169us", + "p99_latency": "862us", + "cpu": "6555.6%", + "memory": "268MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "394.01MB/s", + "bandwidth": "3.61GB/s", "reconnects": 0, - "status_2xx": 22960512, + "status_2xx": 215641136, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/baseline/4096/typev.log b/site/static/logs/baseline/4096/typev.log index c0d584691..9178c2f16 100644 --- a/site/static/logs/baseline/4096/typev.log +++ b/site/static/logs/baseline/4096/typev.log @@ -1,2 +1,3 @@ -typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) -typev HttpArena server listening on :8080 (pipelined + baseline) +typev: loaded '/app/output.tvbc' (v15, 30 functions, entry=29) +typev: workers=64 (sysconf _SC_NPROCESSORS_ONLN=128, TV_WORKERS=64) +typev HttpArena server listening on :8080 diff --git a/site/static/logs/baseline/512/typev.log b/site/static/logs/baseline/512/typev.log index c0d584691..9178c2f16 100644 --- a/site/static/logs/baseline/512/typev.log +++ b/site/static/logs/baseline/512/typev.log @@ -1,2 +1,3 @@ -typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) -typev HttpArena server listening on :8080 (pipelined + baseline) +typev: loaded '/app/output.tvbc' (v15, 30 functions, entry=29) +typev: workers=64 (sysconf _SC_NPROCESSORS_ONLN=128, TV_WORKERS=64) +typev HttpArena server listening on :8080 diff --git a/site/static/logs/json/4096/typev.log b/site/static/logs/json/4096/typev.log index c0d584691..9178c2f16 100644 --- a/site/static/logs/json/4096/typev.log +++ b/site/static/logs/json/4096/typev.log @@ -1,2 +1,3 @@ -typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) -typev HttpArena server listening on :8080 (pipelined + baseline) +typev: loaded '/app/output.tvbc' (v15, 30 functions, entry=29) +typev: workers=64 (sysconf _SC_NPROCESSORS_ONLN=128, TV_WORKERS=64) +typev HttpArena server listening on :8080 diff --git a/site/static/logs/limited-conn/4096/typev.log b/site/static/logs/limited-conn/4096/typev.log index c0d584691..9178c2f16 100644 --- a/site/static/logs/limited-conn/4096/typev.log +++ b/site/static/logs/limited-conn/4096/typev.log @@ -1,2 +1,3 @@ -typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) -typev HttpArena server listening on :8080 (pipelined + baseline) +typev: loaded '/app/output.tvbc' (v15, 30 functions, entry=29) +typev: workers=64 (sysconf _SC_NPROCESSORS_ONLN=128, TV_WORKERS=64) +typev HttpArena server listening on :8080 diff --git a/site/static/logs/limited-conn/512/typev.log b/site/static/logs/limited-conn/512/typev.log index c0d584691..9178c2f16 100644 --- a/site/static/logs/limited-conn/512/typev.log +++ b/site/static/logs/limited-conn/512/typev.log @@ -1,2 +1,3 @@ -typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) -typev HttpArena server listening on :8080 (pipelined + baseline) +typev: loaded '/app/output.tvbc' (v15, 30 functions, entry=29) +typev: workers=64 (sysconf _SC_NPROCESSORS_ONLN=128, TV_WORKERS=64) +typev HttpArena server listening on :8080 diff --git a/site/static/logs/pipelined/4096/typev.log b/site/static/logs/pipelined/4096/typev.log index c0d584691..9178c2f16 100644 --- a/site/static/logs/pipelined/4096/typev.log +++ b/site/static/logs/pipelined/4096/typev.log @@ -1,2 +1,3 @@ -typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) -typev HttpArena server listening on :8080 (pipelined + baseline) +typev: loaded '/app/output.tvbc' (v15, 30 functions, entry=29) +typev: workers=64 (sysconf _SC_NPROCESSORS_ONLN=128, TV_WORKERS=64) +typev HttpArena server listening on :8080 diff --git a/site/static/logs/pipelined/512/typev.log b/site/static/logs/pipelined/512/typev.log index c0d584691..9178c2f16 100644 --- a/site/static/logs/pipelined/512/typev.log +++ b/site/static/logs/pipelined/512/typev.log @@ -1,2 +1,3 @@ -typev: loaded '/app/output.tvbc' (v15, 25 functions, entry=24) -typev HttpArena server listening on :8080 (pipelined + baseline) +typev: loaded '/app/output.tvbc' (v15, 30 functions, entry=29) +typev: workers=64 (sysconf _SC_NPROCESSORS_ONLN=128, TV_WORKERS=64) +typev HttpArena server listening on :8080 From 973a2cc3c16d3877ce620ac57f06f3deccda410c Mon Sep 17 00:00:00 2001 From: praisethemoon Date: Sun, 17 May 2026 23:00:35 +0200 Subject: [PATCH 10/11] typev: revert back to 4 shards --- .../typev/bundle/benchmark-code/src/main.tc | 2 +- frameworks/typev/bundle/output.tvbc | Bin 16501 -> 16501 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/frameworks/typev/bundle/benchmark-code/src/main.tc b/frameworks/typev/bundle/benchmark-code/src/main.tc index 9f9bcb9af..d36587c72 100644 --- a/frameworks/typev/bundle/benchmark-code/src/main.tc +++ b/frameworks/typev/bundle/benchmark-code/src/main.tc @@ -194,7 +194,7 @@ fn accept_loop() -> void { fn main() -> uint { println("typev HttpArena server listening on :8080") - foreach i in 0, 31, 1 { + foreach i in 0, 3, 1 { spawn accept_loop() } diff --git a/frameworks/typev/bundle/output.tvbc b/frameworks/typev/bundle/output.tvbc index ed59921b93715fcd6ff0f0fdccba6abb4530f821..768b58e93fbadec2a597f818bfe92b9fdf710ef6 100644 GIT binary patch delta 784 zcmYk4OKTKC6opS!-EtD0?u;=`KqD#`V|*m)WJnM}w>^_Fz6e5KKyalvjwp&EOyeW? zLWN!kO}kcn5t$z#xN|i>LHDjBxbyU-1!?M@)8D;y3aWcOHtVrj`(7O{;osx%-_!qD zZB0x^=o&cVfR43!*NTuih!JH1A@hx7xi)K}WVN=myM?g;VH+-%QVR%CYQx0TfG34g zUQB-1R(3?dHUQgUFoDt_;1MGT+Nj17L`i4vt7-IS>OTxa!}*J*nRMsB8JE-+rcATH zvXEIj943E`)q9tj0$kw|Tvgv;HR!6puKb4jo62vgzpea^`n$^SsgGF&_tigOHF%_c zRrzD}Pn17X|NMeBUg*n9ZM;^WvJN3r|AtlYR>?c{@0EXGt>BaT&&t=-f5~~@AAe|j zsojIDY~dM@Vv-)#TaVQ4E%*lVse+ft_Z57Ie7fMn^H7JS<$+5X)Kda%l{y%yFYIWGh4R8^{|~^85yJJ`Upn?@v(; jb&1Lw^QWm5>KW=T>RIYZ>N#pmJ&%E}f+uOh699cxJdFXAVj_m6EOq! z4yCvh|E@0YsRO$J?1sT8^8J91C?RO05=l^xS7tU%qc>IiVc0aBy<(d2>g-oD7FXvc zOtZH<*R^(_6#qF{>s)6S;08wlHOXQG$0y~_>c1#&roQJ-uh~wz z+uografUh1w9E_4GV>yHoVma}%e=&ln2Si Date: Sun, 17 May 2026 21:11:58 +0000 Subject: [PATCH 11/11] Benchmark results: typev --- site/data/baseline-4096.json | 16 ++++++++-------- site/data/baseline-512.json | 16 ++++++++-------- site/data/json-4096.json | 18 +++++++++--------- site/data/limited-conn-4096.json | 18 +++++++++--------- site/data/limited-conn-512.json | 18 +++++++++--------- site/data/pipelined-4096.json | 14 +++++++------- site/data/pipelined-512.json | 12 ++++++------ 7 files changed, 56 insertions(+), 56 deletions(-) diff --git a/site/data/baseline-4096.json b/site/data/baseline-4096.json index 9e252e2a2..afab66c36 100644 --- a/site/data/baseline-4096.json +++ b/site/data/baseline-4096.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 1916753, - "avg_latency": "243us", - "p99_latency": "1.11ms", - "cpu": "6439.9%", - "memory": "292MiB", + "rps": 1918007, + "avg_latency": "199us", + "p99_latency": "1.20ms", + "cpu": "6737.7%", + "memory": "384MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "164.46MB/s", - "input_bw": "148.06MB/s", + "bandwidth": "164.58MB/s", + "input_bw": "148.16MB/s", "reconnects": 0, - "status_2xx": 9583767, + "status_2xx": 9590035, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/baseline-512.json b/site/data/baseline-512.json index 5cd57b6fd..7162b2795 100644 --- a/site/data/baseline-512.json +++ b/site/data/baseline-512.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 1865595, - "avg_latency": "134us", - "p99_latency": "795us", - "cpu": "6472.4%", - "memory": "275MiB", + "rps": 1871533, + "avg_latency": "124us", + "p99_latency": "680us", + "cpu": "6498.3%", + "memory": "251MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "160.08MB/s", - "input_bw": "144.11MB/s", + "bandwidth": "160.59MB/s", + "input_bw": "144.57MB/s", "reconnects": 0, - "status_2xx": 9327978, + "status_2xx": 9357667, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/json-4096.json b/site/data/json-4096.json index f948a7092..7756cbabc 100644 --- a/site/data/json-4096.json +++ b/site/data/json-4096.json @@ -956,19 +956,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 544745, - "avg_latency": "5.58ms", - "p99_latency": "11.40ms", - "cpu": "3102.4%", - "memory": "442MiB", + "rps": 1077677, + "avg_latency": "3.34ms", + "p99_latency": "1.36ms", + "cpu": "6730.8%", + "memory": "402MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "1.84GB/s", - "input_bw": "25.98MB/s", - "reconnects": 108821, - "status_2xx": 2723726, + "bandwidth": "3.63GB/s", + "input_bw": "51.39MB/s", + "reconnects": 215192, + "status_2xx": 5388386, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/limited-conn-4096.json b/site/data/limited-conn-4096.json index 085e69c99..977c07f35 100644 --- a/site/data/limited-conn-4096.json +++ b/site/data/limited-conn-4096.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 663145, - "avg_latency": "5.67ms", - "p99_latency": "104.30ms", - "cpu": "3258.4%", - "memory": "408MiB", + "rps": 1282861, + "avg_latency": "3.13ms", + "p99_latency": "194.90ms", + "cpu": "6597.3%", + "memory": "304MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "56.90MB/s", - "input_bw": "51.23MB/s", - "reconnects": 331619, - "status_2xx": 3315725, + "bandwidth": "110.08MB/s", + "input_bw": "99.10MB/s", + "reconnects": 641598, + "status_2xx": 6414306, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/limited-conn-512.json b/site/data/limited-conn-512.json index c90d315f9..fe37b045b 100644 --- a/site/data/limited-conn-512.json +++ b/site/data/limited-conn-512.json @@ -1208,19 +1208,19 @@ { "framework": "typev", "language": "Type-C", - "rps": 288546, - "avg_latency": "1.75ms", - "p99_latency": "72.60ms", - "cpu": "1386.9%", - "memory": "178MiB", + "rps": 912050, + "avg_latency": "552us", + "p99_latency": "20.10ms", + "cpu": "4779.8%", + "memory": "211MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "24.76MB/s", - "input_bw": "22.29MB/s", - "reconnects": 144278, - "status_2xx": 1442730, + "bandwidth": "78.26MB/s", + "input_bw": "70.45MB/s", + "reconnects": 456012, + "status_2xx": 4560254, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/pipelined-4096.json b/site/data/pipelined-4096.json index a60c039a2..17274fdd6 100644 --- a/site/data/pipelined-4096.json +++ b/site/data/pipelined-4096.json @@ -1145,18 +1145,18 @@ { "framework": "typev", "language": "Type-C", - "rps": 47909920, - "avg_latency": "247us", - "p99_latency": "1.15ms", - "cpu": "6198.4%", - "memory": "555MiB", + "rps": 48398726, + "avg_latency": "246us", + "p99_latency": "1.18ms", + "cpu": "6254.3%", + "memory": "355MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "4.01GB/s", + "bandwidth": "4.06GB/s", "reconnects": 0, - "status_2xx": 239549600, + "status_2xx": 241993632, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/pipelined-512.json b/site/data/pipelined-512.json index 3113b2aec..6d9ede5ba 100644 --- a/site/data/pipelined-512.json +++ b/site/data/pipelined-512.json @@ -1145,18 +1145,18 @@ { "framework": "typev", "language": "Type-C", - "rps": 43128227, + "rps": 47930259, "avg_latency": "169us", - "p99_latency": "862us", - "cpu": "6555.6%", - "memory": "268MiB", + "p99_latency": "981us", + "cpu": "6303.1%", + "memory": "276MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "3.61GB/s", + "bandwidth": "4.02GB/s", "reconnects": 0, - "status_2xx": 215641136, + "status_2xx": 239651296, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0