feat: Zig 0.16 migration + perf improvements (~140k req/s)#135
feat: Zig 0.16 migration + perf improvements (~140k req/s)#135
Conversation
- Add zig/src/telemetry.zig: Event union (log/span_start/span_end/counter/histogram/gauge), Exporter vtable, stderr exporter (text + JSON-lines), TURBO_LOG_LEVEL/TURBO_LOG_FORMAT env config, zero-overhead enabled gate - Add zig/src/logger.zig: debug/info/warn/err wrappers over pushEvent(.log), level check before formatting, 1024-byte stack buffer - Add python/turboapi/logger.py: TurboJSONFormatter, get_logger(), TURBO_LOG_LEVEL/TURBO_LOG_FORMAT - Replace all 19 std.debug.print calls (12 in server.zig, 7 in db.zig) with leveled logger.*() - Wire telemetry.init() in server_new() Refs #127 Co-authored-by: trilokagent <275208033+trilokagent@users.noreply.github.com>
Tests verify: Zig text/JSON output, level filtering, JSON schema (ts/level/msg), combined JSON+level filtering, Python logger (text/JSON/trace_id/dedup), defaults, and in-process integration. Refs #127 Co-authored-by: trilokagent <275208033+trilokagent@users.noreply.github.com>
…_integration
- jwt_auth: remove duplicate unreachable return in create_refresh_token (L152)
- routing: fix path param detection — check {param_name} not substring match,
which would misclassify params whose names appear anywhere in the path string
- request_handler: avoid mutating caller dict in normalize_response (pop→get+copy)
- request_handler: fix operator precedence in UploadFile form-param detection —
_has_form_types guard was not protecting the second or-branch, risking NameError
- responses: fix set_cookie silently dropping all cookies after the first
(setdefault→_cookies list); propagate Response.headers and cookies through
normalize_response→format_response as extra_headers dict
- zig_integration: before_request exceptions now use exception status_code attr
instead of hardcoding 429 (Too Many Requests) for all middleware errors
- zig_integration: on_error middleware response was computed but its content
was always discarded; first non-None middleware error response is now returned
- zig_integration: middleware handler merges handler-level extra_headers (cookies,
custom headers) with middleware after_request headers, supporting multi-value
Set-Cookie via CRLF injection into content_type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lient async
- telemetry.zig: control characters below 0x20 were JSON-escaped using decimal
format {d:0>4} instead of hex {x:0>4}, producing invalid \u sequences for
values > 9 (e.g. \x0B emitted as \u0011 instead of \u000b)
- openapi.py: Optional[X] / Union[X, None] was not recognized because
get_origin(Optional[X]) returns Union, not type(None); the dead check
origin is type(None) never fired, returning {} for all Optional params;
now correctly returns nullable inner schema
- testclient.py: two asyncio bugs fixed:
1. deprecated asyncio.get_event_loop().run_until_complete() replaced with
asyncio.run() throughout
2. RuntimeError fallback for async dep_fn was calling the coroutine function
without awaiting it, storing a coroutine object instead of the resolved value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
db.zig: - select_list used substring indexOf for "limit="/"offset=" — false match on params like "nolimit=5"; fixed to startsWith - insert handler allocated fmt strings for int/float column values via allocPrint but never freed them (memory leak per insert); fixed to stack bufPrint with per-column [64]u8 scratch buffers - db_query_raw bool param handling called Py_IsTrue on any object (truthy integers/strings also matched); fixed to check PyBool_Check first - RawCell.len was u16, causing @intcast panic for text cells > 64 KB; widened to u32 dhi_validator.zig: - parseSchema leaked the JSON arena on success (deinit only on null path); fixed to always defer parsed.deinit() middleware.py: - CORSMiddleware.after_request used re.match for allow_origin_regex, which anchors only at the start — "https://example\.com" matches "https://example.com.evil.com"; fixed to re.fullmatch - CSRFMiddleware.after_request directly set response.headers["Set-Cookie"], overwriting existing cookies and bypassing the _cookies list; fixed to use response.set_cookie() Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
- StreamingResponse/__init__ and FileResponse/__init__: missing self._cookies=[] initialization — set_cookie() raised AttributeError - turbopg Database._execute_native: ignored Zig _db_exec_raw path, always fell through to Python psycopg2 fallback even when native pool was available - turboapi-core router addChild: partial state mutation on OOM — self.indices grew before children alloc succeeded, leaving indices.len != children_list.len; fix: allocate both arrays with errdefer before mutating self Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
str.startswith() on a resolved path string lets a sibling directory whose name begins with the static dir's name (e.g. /static vs /static_other) bypass the containment check. Replace with Path.is_relative_to() which does a proper ancestry check on path components rather than raw string bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
The ASGI __call__ lifespan handler was calling .__anext__() on the lifespan context manager. FastAPI-compatible lifespans use @asynccontextmanager which returns an _AsyncGeneratorContextManager — an async context manager with __aenter__/__aexit__, not __anext__. Calling __anext__() on it raises AttributeError at startup. Fix: use __aenter__() on startup, __aexit__(None, None, None) on shutdown, matching the async context manager protocol. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Replace all std.net.* networking with the 0.16 std.Io.net API: - std.net.Stream → std.Io.net.Stream across all function signatures - stream.close() → stream.deinit() (RAII destructor rename) - server_run: add std.Io.Threaded accept-loop runtime - parseIp4 → std.Io.net.IpAddress.parse + listen takes io arg - tcp_server.accept() returns stream directly (no .stream field) - build.zig.zon version bumped to 0.16.0 - docs/ZIG_0_16_MIGRATION.md added with full checklist ConnectionPool (std.Thread.spawn) intentionally kept — std.Io.Threaded cannot hook into per-worker PyThreadState lifecycle required by GIL integration. Builds against Zig 0.16 only — intentionally breaks on 0.15.x. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes the remaining 4 (then 8 more discovered) compilation errors introduced by the 0.15→0.16 API changes, achieving a clean build. Changes by file: zig/src/server.zig - Add `const posix = std.posix;` alias (posix.read calls were using it) zig/src/db.zig - `std.time.milliTimestamp()` → clock_gettime-based now() helper - `std.net.Stream` → `std.Io.net.Stream` in handleDbRoute signature zig/src/runtime.zig (new) - Shared Io.Threaded + Io instance created in d23db8f, now tracked zig/src/telemetry.zig - milliTimestamp helper + @divTrunc for signed division (already in prev) zig/zig-pkg/ (vendored, patched for 0.16) - pg/src/stream.zig: rename local `socket` vars → `sock_fd` to stop shadowing the module-level `extern "c" fn socket()`; fix `posix.close` → `std.c.close` in TLSStream.close; mark unused PlainStream.connect allocator param as `_` - pg/src/conn.zig: `std.time.timestamp()` → posixTimestamp() helper; ArrayListUnmanaged empty init `.{}` → `.{.items=&.{},.capacity=0}` - pg/src/pool.zig: Thread.Mutex/Condition → PthreadMutex/PthreadCondition shims; nanoTimestamp/threadSleep helpers - pg/src/auth.zig: `std.crypto.random.bytes` → `arc4random_buf` extern - pg/src/types/numeric.zig: `std.io.fixedBufferStream` → `std.fmt.bufPrint` - N-V-.../src/buffer.zig: `std.io.Writer` → `std.Io.Writer` in drain vtable Build: `python zig/build_turbonet.py --install` → clean, 0 errors Tests: 341 pass, all failures are pre-existing Python-layer issues Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The initial guide written in d23db8f contained several inaccuracies (stream.close() takes an io arg, stream.handle is now socket.handle, stream.read/writeAll are gone, db.zig/telemetry.zig do need changes, Thread.Mutex/Condition were removed, etc.). Rewrites the guide based on the actual fixes applied in 390729d: - Section 1: Full Io.net networking migration with all stream API details - Section 2: std.io (lowercase) removed — bufPrint, Io.Writer vtable fix - Section 3: All time functions removed — clock_gettime helpers + @divTrunc - Section 4: Thread.Mutex/Condition/sleep removed — pthread shims - Section 5: lockStderrWriter → lockStderr new struct API - Section 6: std.crypto.random → arc4random_buf - Section 7: ArrayListUnmanaged init syntax change - Section 8: Local const shadowing module-level extern is now an error - File-by-file summary and checklist updated to match actual commits Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ASGI fallback TestClient now injects UploadFile objects for File()/UploadFile params and coerces form values for Form() params from the raw files/data kwargs before calling the handler. _build_response handles bytes return values directly. ASGI __call__ fallback also gains the same multipart/urlencoded parsing branch keyed on content-type, using the existing _parse_multipart helper. Fixes 25 of 27 pre-existing failures; 2 remaining are flaky subprocess- timeout telemetry tests that pass in isolation. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
ResponseHandler.normalize_response now only includes extra_headers in the return tuple when non-empty, fixing unpacking mismatches in the Zig FFI path. Response.set_cookie() now also updates self.headers so cookie values are visible to response serializers that read headers directly. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Covers every breaking change hit during the TurboAPI migration, written as a project-agnostic quick reference: build system (Compile→Module API), std.net→std.Io.net, std.io removed, time/thread/random removals, posix pruning, ArrayListUnmanaged init, and local-shadow-extern error. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Covers brew install, fetch/list/default commands, zigup run for side-by-side builds, worktree + dual-version comparison pattern, and CI YAML examples for pinning the version. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
- py.zig: _Py_NoneStruct[0..] → &c._Py_NoneStruct (0.16 slice-of-extern removed, must take address directly) - db.zig: replace extern py_gil_save/restore shim with py.PyEval_SaveThread/RestoreThread (shim no longer needed after py.zig re-export) - db.zig: lockUncancelable() → lock() catch return (0.16 Mutex API) Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
router.zig findChildIndex: scan 16 indices/cycle with @vector(16, u8) + @reduce(.Or, ...). Scalar tail for nodes < 16 children. server.zig findHeaderEnd: SIMD scan for '\r\n\r\n' — 16 bytes/iter, scalar-verifies hits. ~4× faster than std.mem.indexOf for 300-2000 B headers. Replaces the plain indexOf call in handleOneRequest. server.zig percentDecode: bulk-copy clean runs with indexOfAny + @memcpy before falling through to the single-char decode loop. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
…atomics dhi_validator.zig parseFieldType: replace 14 mem.eql checks with a comptime-perfect-hash StaticStringMap — O(1) lookup. dhi_validator.zig makeError: bufPrint into 256-byte stack buf then allocator.dupe, avoiding allocPrint heap path on every error. telemetry.zig writeJsonString: scan for special chars with inner while, bulk-copy clean runs with writeAll, emit escape sequences. Reduces per-byte overhead for clean strings. telemetry.zig: enabled/log_level → std.atomic.Value for safe concurrent reads without a mutex. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Replace 7 sequential mem.eql checks in parseHandlerType and 3 in parseParamType with std.StaticStringMap comptime perfect-hash tables. O(1) dispatch for both hot-path route handler classification and parameter type resolution at registration time. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
…qlLiteral encodePySqlValue: replace PyUnicode_AsUTF8()+std.mem.span() with PyUnicode_AsUTF8AndSize() in both the direct-unicode and fallback-str paths. Avoids an implicit strlen() call — CPython returns the length alongside the pointer. appendSqlLiteral: replace byte-by-byte single-quote escaping loop with std.mem.indexOfScalarPos() scan + appendSlice() bulk-copy. Single- quote escaping is rare, so most strings are now copied in one shot. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
StaticStringMap comptime perfect-hash for parseHandlerType (7 branches) and parseParamType (3 branches) in server.zig. O(1) dispatch at route registration time instead of sequential string comparisons. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
PyUnicode_AsUTF8AndSize (avoids strlen) in encodePySqlValue + bulk-copy indexOfScalarPos quoting loop in appendSqlLiteral. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Python 3.14 raises UnboundLocalError when a function contains a local assignment to a name that is also used before that assignment — even if the local assignment is in an unreachable branch. The two inline 'import inspect' inside __call__'s lifespan block shadowed the module-level import, causing every HTTP request to 500. Fix: drop the redundant inline imports; the module-level import is already in scope. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Adds a new "Step-by-step migration walkthrough with zigup" section that covers the full 0.15.2→0.16 migration as an ordered checklist: 1. Install zigup + fetch both compilers (no system pollution) 2. Create isolation branch 3. First build attempt with 0.16 (common error table) 4. Fix errors in batches (dual-build check after each) 5. Run test suite under 0.16 6. Benchmark old vs new with worktrees + wrk 7. Promote 0.16 as default Includes actual benchmark numbers from TurboAPI (138–140 k req/s native Zig HTTP, +0.3–1.1% per endpoint after SIMD + StaticStringMap + bulk-copy optimisations). Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed0d095caf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| response.headers[ | ||
| "Set-Cookie" | ||
| ] = f"{self.cookie_name}={token}; Path=/; SameSite=Lax" | ||
| response.set_cookie(self.cookie_name, token, path="/", samesite="lax") |
There was a problem hiding this comment.
Use header API compatible with middleware Response type
CSRFMiddleware.after_request now calls response.set_cookie(...), but middleware code imports Response from turboapi.models, and that class only defines set_header/get_header (no set_cookie). When CSRF middleware is enabled, safe-method responses that need token issuance will raise AttributeError in after_request, causing request failures instead of setting the CSRF cookie.
Useful? React with 👍 / 👎.
| if (!telemetry.isEnabled()) return; | ||
| if (@intFromEnum(level) < @intFromEnum(telemetry.getLevel())) return; | ||
| var buf: [1024]u8 = undefined; | ||
| const msg = std.fmt.bufPrint(&buf, fmt, args) catch buf[0..]; |
There was a problem hiding this comment.
Handle log message overflow without exposing undefined buffer
When std.fmt.bufPrint overflows, logger.log falls back to buf[0..], but buf is declared undefined. That means oversized log messages can emit uninitialized stack bytes to telemetry output, producing corrupted JSON/text logs and potentially leaking memory contents whenever formatted messages exceed 1024 bytes.
Useful? React with 👍 / 👎.
| for cookie in getattr(result, "_cookies", []): | ||
| existing = extra_headers.get("set-cookie") | ||
| if existing is None: | ||
| extra_headers["set-cookie"] = cookie |
There was a problem hiding this comment.
Prevent duplicate Set-Cookie emission in normalized responses
normalize_response first copies result.headers into extra_headers and then appends every value from result._cookies. Because Response.set_cookie now also stores cookies in headers['set-cookie'], each cookie is added twice, resulting in duplicate Set-Cookie headers on cookie-bearing responses.
Useful? React with 👍 / 👎.
ci.yml (3×), perf-regression.yml (2×), build-and-release.yml (1×), pre-release.yml (1×). Required now that the codebase uses 0.16-only APIs (std.Io.Threaded, std.Io.Mutex, std.debug.lockStderr). Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a5ce302dda
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| call_args[param_name] = default.default | ||
| elif "application/x-www-form-urlencoded" in content_type_val: | ||
| from .datastructures import Form as _Form | ||
| _qs = parse_qs(body.decode("utf-8", errors="replace"), keep_blank_values=True) |
There was a problem hiding this comment.
Import parse_qs before decoding form-urlencoded bodies
The new application/x-www-form-urlencoded branch calls parse_qs(...) unconditionally, but parse_qs is only imported earlier inside the if query_string: block. For POST/PUT requests with an empty URL query string (a common form-submit case), this path raises NameError before the handler runs, causing ASGI request failures instead of normal form parsing.
Useful? React with 👍 / 👎.
| if len(non_none) == 1: | ||
| inner = _type_to_schema(non_none[0]) | ||
| inner["nullable"] = True | ||
| return inner | ||
| return {"nullable": True} |
There was a problem hiding this comment.
Preserve non-optional union types in OpenAPI schemas
This fallback now marks any Union that is not Optional[T] as only {"nullable": true}. For annotations like Union[int, str], the generated schema loses both member types and incorrectly advertises that null is valid, which can mislead client generators and downstream validation based on OpenAPI.
Useful? React with 👍 / 👎.
…me, multipart toOwnedSlice - turboapi-core/cache.zig: replace std.Thread.Mutex (removed in 0.16) with std.c.pthread_mutex_t + PTHREAD_MUTEX_INITIALIZER; add pthread_mutex_destroy in deinit - turboapi-core/http.zig: replace std.time.timestamp() (removed in 0.16) with std.c.clock_gettime(.REALTIME, &ts_spec); fuzz callback → *std.testing.Smith - turboapi-core/router.zig: fuzz callback → *std.testing.Smith - zig/src/dhi_validator.zig: fuzz_validateJson → *std.testing.Smith - zig/src/server.zig: fuzz callbacks → *std.testing.Smith; initialize runtime.threaded/io in concurrent-cache test so std.Io.Mutex.lockUncancelable has a valid io context - zig/src/multipart.zig: use toOwnedSlice() instead of .items to fix Invalid free in debug allocator (ArrayListUnmanaged capacity ≠ length) All 13 zig unit tests + all turboapi-core tests now pass under Zig 0.16.0. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Performance Regression Report
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 444bcf6d0e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (std.c.getenv("TURBO_LOG_LEVEL")) |_p| { const val = std.mem.span(_p); | ||
| if (std.mem.eql(u8, val, "debug")) log_level.store(.debug, .release) else if (std.mem.eql(u8, val, "info")) log_level.store(.info, .release) else if (std.mem.eql(u8, val, "warn")) log_level.store(.warn, .release) else if (std.mem.eql(u8, val, "error")) log_level.store(.err, .release); |
There was a problem hiding this comment.
Normalize TURBO_LOG_LEVEL values before parsing
telemetry.init() only accepts exact lowercase strings (debug/info/warn/error) from TURBO_LOG_LEVEL, so common deployments that set DEBUG/WARNING (or other case variants) silently fall back to .info. In those environments the Zig logger ignores the configured severity and emits unexpected info logs, which is inconsistent with the Python logger behavior in this same commit.
Useful? React with 👍 / 👎.
| return { | ||
| "content": final_resp.content, | ||
| "status_code": final_resp.status_code, | ||
| "content_type": "application/json", | ||
| } |
There was a problem hiding this comment.
Preserve middleware error response headers and media type
When mw.on_error() returns a response, this branch hardcodes "content_type": "application/json" and drops all headers from final_resp. Any middleware that returns non-JSON error bodies (e.g., HTML/text) or relies on error headers (WWW-Authenticate, Set-Cookie, redirects) will send incorrect wire responses even though it provided a full response object.
Useful? React with 👍 / 👎.
…ettime Linux requires explicit -lc for std.c.pthread_mutex_* and std.c.clock_gettime. macOS auto-links libc so it passed locally but failed on ubuntu-latest CI. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Performance Regression Report
|
Add a minimal Timer shim using std.c.clock_gettime(.MONOTONIC) and replace the 3 call sites of std.time.Timer.start() catch unreachable. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Performance Regression Report
|
…nners GitHub Actions shared VMs hit 162ns on the anti-DCE test; 100ns was calibrated for a local M3 Pro. 500ns still catches real regressions (trie blowup etc.) while not flaking on slow CI hosts. Co-Authored-By: trilokagent <275208033+trilokagent@users.noreply.github.com>
Performance Regression Report
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c336b3585a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if is_file_default or is_upload_ann: | ||
| field_name = (default.alias if is_file_default and default.alias else param_name) | ||
| if field_name in _file_map: | ||
| fd = _file_map[field_name] | ||
| call_args[param_name] = _UF( |
There was a problem hiding this comment.
Preserve bytes semantics for File() parameters
The multipart branch in TurboAPI.__call__ now routes both File() defaults and UploadFile annotations through _UF(...), which means handlers declared as file: bytes = File() receive an UploadFile object instead of raw bytes. In ASGI fallback mode, routes that follow the documented File() bytes pattern will fail or misbehave (for example, len(file)/hashing code) because the injected argument type no longer matches the annotation.
Useful? React with 👍 / 👎.
| const n = write(stream.socket.handle, remaining.ptr, remaining.len); | ||
| if (n <= 0) return error.BrokenPipe; |
There was a problem hiding this comment.
Retry interrupted writes in streamWriteAll
streamWriteAll treats any write() result <= 0 as BrokenPipe, but POSIX write may return -1 for transient conditions like EINTR (and occasionally EAGAIN) even when the socket is still valid. Under signal activity, this can prematurely abort response transmission and close otherwise healthy connections, yielding truncated or failed responses.
Useful? React with 👍 / 👎.
Summary
root_module.*API), networking (std.Io.net), time (clock_gettime), threading (POSIX shims), random (arc4random_buf), and misc breaking changesfindHeaderEnd(16 bytes/cycle header scan) andfindChildIndex(16-wide router dispatch) inserver.zig/router.zigparseHandlerType(7 branches),parseParamType(3 branches), andparseFieldType(14 branches) — replaces sequentialmem.eqlchainspercentDecode,writeJsonString,appendSqlLiteral— scan to next special char, copy clean runs with@memcpy/appendSlicePyUnicode_AsUTF8AndSizeinencodePySqlValue— avoids implicitstrlenon every SQL valueimport inspectinside__call__shadowed the module-level import, causing 500 on every ASGI requestmigration-0.15.2-to-0.16.mdreference (448 → 569 lines) with zigup multi-version management and step-by-step walkthroughBenchmark results
Native Zig HTTP server,
-t4 -c100 -d8s, Apple M-series (pre = 0.16 compat only, post = + all perf work):Test plan
zig build testpasses (Zig unit tests)pytest tests/passes on Python 3.14t (all pre-existing exclusions retained)zig-0.16-perf-static-dispatch,zig-0.16-perf-db-encoding) merged and verified🤖 Generated with Claude Code