codec: precompute per-field writers for singular scalar/enum/message (~+50% small-msg encode)
pb.finalize_message now calls codec.compile_writers(desc), which
attaches `f._writer` to each field whose shape we can specialize:
singular scalar, singular enum, singular message — i.e. not maps,
not repeated, not oneof. Each writer is a monomorphic closure that
knows its tag bytes, encoder function, and default predicate. The
encode_message hot loop calls writer(data, out) per field and only
falls through to encode_field for shapes we haven't specialized.
This eliminates the per-field `encode_field` dispatch chain
(kind/proto_type branch + `is_default_scalar` call), which was the
source of the remaining trace bridges in runtime mode (codec.lua:41
and codec.lua:110 in `make jit-trace` output).
Effect (bench/bench.lua, hello.Person):
runtime/10B encode: 5.0 → 8.1 MB/s (+62%)
runtime/100B encode: 48.2 → 75.2 MB/s (+56%)
runtime/1KB encode: 49.5 → 56.1 MB/s (+13%)
runtime/10KB+ encode: unchanged (dominated by repeated-field
iteration — not yet specialized)
full mode encode: +10% across small sizes (writer closures also
help when codegen calls back into the runtime)
decode: unchanged
alloc/op: unchanged (bench-compare clean)
bridges across 10 jit-trace runs: 7 → 4
wire: inline decode_varint 1-byte fast path at all decoders (~2× decode)
decode_tag, decode_len, decode_int32/uint32/int64/uint64/sint32/sint64/
bool, and the VARINT/LEN branches of skip_field each now read the first
byte directly, handle 0..127 in straight-line code, and call into
decode_varint only for multi-byte values. The duplicated 3 lines per
call site are the cost of avoiding LuaJIT 2.1's side-trace-returning-
from-inlined-call limitation: with the fast path inlined, side traces
off the parent decoder's hot guard stay in the caller's own frame and
stitch back cleanly instead of bridging to interpreter dispatch.
Effect (bench/bench.lua, hello.Person across 5 sizes):
full mode decode: 2.1×–2.4× throughput (104→220 .. 14→33 MB/s)
runtime mode decode: 2.0×–2.1× throughput (90→175 .. 12→25 MB/s)
encode: unchanged (only decode paths were touched)
alloc/op: unchanged (bench-compare clean)
bridges: 27 → 7 across 10 jit-trace runs (-74%);
remaining are encoder-side (codec.lua:41/110 in runtime mode)
M6: trace stability gate + two fixes
Add `make jit-trace` (`bench/jit_trace.lua`) — a standalone tarantool
script that attaches a `jit.attach('trace')` listener over each hot
encode/decode path and asserts no aborts in our source files fall into
the fatal set (NYI bytecode, blacklisting, persistent type instability).
Runs outside luatest because on macOS arm64 the test framework exhausts
JIT mcode pages before the test body runs, masking real abort reasons.
Two fixes shipped to make all 13 scenarios pass:
- `decode_varint` grew a 1-byte fast path. Before, calling it from a
hot decode loop pulled an inner `while true do` into the caller's
root trace, which got blacklisted after enough retries.
- `pb.finalize_message` now precomputes `desc.oneofs_list` (array
form) and the runtime-mode codec iterates it with ipairs instead
of `pairs(desc.oneofs)`. `pairs()` over a hash-keyed table compiles
to bytecode ISNEXT, which is NYI in LuaJIT 2.1.
The gate also reports interpreter-bridge counts as a benchmark-quality
metric. Decoders show 0-4 bridges per run depending on JIT timing —
caused by side traces returning from inlined `decode_varint` calls,
which LuaJIT 2.1 can't stitch back cleanly. Small per-call overhead on
the multi-byte slow path, structural to the engine.
Scope caveat: map fields encode via `pairs()` and remain off-trace —
pinned by the gate's last scenario so we notice if upstream lifts the
restriction.
Initial commit: protoc-gen-tarantool plugin + pb runtime
A protoc plugin (Go) and a pure-Lua + LuaJIT-FFI runtime that give
Tarantool a complete proto3 + gRPC stack. Two codegen modes (full
inline / runtime descriptor), 226-test luatest suite, 18-fixture
mainline-protoc interop corpus, JSON codec, well-known types,
gRPC client/server factories, runtime .proto parser, microbench
harness with allocation regression gate.
Covers PLAN.md M1-M5. Module is `pb` (not `protobuf`) to avoid
colliding with Tarantool's built-in encode-only `protobuf` module.