~bigbes/tarantool

tarantool-protobuf

ref: 7edbf9736526a4963ac6f8b9bc0ef31f5b847c9a tarantool-protobuf/bench/jit_trace.lua -rw-r--r-- 16.2 KiB
4b91822d — Eugene Blikh 3 months ago
bench/jit_trace: harden mcode arena + jit.off the listener

Two infra fixes for the trace-stability gate, both reproduced on
Tarantool 3.8.0-entrypoint / LuaJIT 2.1.0-beta3 / macOS arm64.

1. Default JIT mcode arena (sizemcode=32K, maxmcode=512K) is too
   small for our hot-path codegen footprint. Roughly 1 in 10 runs
   the gate reports every check as 'stops=0' with no diagnostic —
   indistinguishable from a real JIT topology regression. jit.v
   shows 'failed to allocate mcode memory at hello_pb.lua:890'
   (the packed lucky_numbers varint loop). Calling
   jit.opt.start('sizemcode=64','maxmcode=4096') at the top of
   the gate gives the arena enough headroom to hold both encoder
   and decoder bodies for the proto3 + proto2 fixtures.

2. The trace listener callback itself can become hot enough to
   be JIT-compiled, which then races with the function being
   recorded (recording-while-recording). Reproduced with a fat
   cb that appends event tuples to a table: starts go up but
   stops drop to ~0. Calling jit.off(cb) on the listener
   prevents this. The current cb body happens to dodge this
   because its branches keep call sites polymorphic — but it's
   fragile; any future extension (per-event timing, pc context)
   would re-trigger the bug. Adds the jit.off as defense.

Verified 20/20 consecutive runs now report 37/37 passing (was
intermittently 0/37 before). Also drops a now-stale "(PLAN.M6)"
reference from the header comment.

beads-tarantool-protobuf-3o2
2656c977 — Eugene Blikh 3 months ago
bench,codec: pin proto2 paths, kill pairs() on extension hot path

JIT trace gate gains 12 proto2-specific checks (required encode/decode,
groups singular + repeated, extension encode/decode). The extension-encode
check fired NYI on bytecode 72 (ISNEXT) — pairs() over the new
extensions_by_full_name hash is the same trace-abort that gates map
encode. Fix: register_extension now also appends to extensions_list,
an array view. codec.encode_message, text.emit_message and
json.encode_message all switch to ipairs over the list. Hash tables
stay around for O(1) lookups in decode (extensions_by_id) and bracket-
name resolution (extensions_by_full_name).

Inline (full-mode) codegen learns to walk extensions too: before this
commit the inline encoder skipped t._extensions entirely (only the
runtime codec walked it), so a generated _encode silently dropped any
extension data set on the message. Add the array-walk after the field
loop and a decode_extension dispatch in the unknown-tag branch — the
inline path now matches runtime byte-for-byte.

bench/bench.lua parameterizes over a FIXTURES list so the baseline can
cover both hello.Person and a new proto2_basic.BenchPayload fixture
(required + group + extension + repeated). baseline.json restructured
under "schemas": [{schema, results}, …]; compare walks both. Fresh
numbers committed.

740/740 luatest cases pass; JIT gate 37/37; conformance 2806 binary+JSON
and 434 text-format, both 0 failures.
19c8560e — Eugene Blikh 3 months ago
bench: per-helper wire bench + shape-variety bench + multi-byte jit gates

Three additions so future perf regressions are visible at the right
granularity, not just averaged out by the single Person shape in
bench/bench.lua.

bench/wire_bench.lua (new). Microbenches every wire helper in
isolation — encode/decode for varint at 1/2/3/5 bytes, zigzag, fixed
widths, float/double, LEN, tag, skip_field, utf8 validator. Used when
tuning wire.lua to confirm a change moved the helper-level ns/op as
expected (e.g. encode_varint(150) 529 ns -> 68 ns from the 2-byte
fast path).  Run via `make bench-wire`. No baseline, no regression
gate — this is a manual inspection tool.

bench/shapes_bench.lua (new). Runs encode + decode against a handful
of fundamentally different Person / Event / Result shapes —
scalar-heavy, packed-int (100 and 1000 elements), nested-friends
(10 and 100), maps (scalar-valued and message-valued), oneof, and
WKT-heavy. Each shape dials one knob up so cost attribution stays
clean. Shows alloc B/op alongside throughput. Run via
`make bench-shapes`. Caught the packed-int / multi-byte-varint
opportunity that bench/bench.lua (all 1-byte varints, fixed
shape) doesn't surface.

bench/jit_trace.lua + 4 gates. Two new fixtures per mode that
specifically exercise encode_varint_slow's 2/3-byte Lua-number paths
and decode_string's multi-byte LEN fallback (200-byte name + packed
ints in [150..500000]). If a future change pushes encode_varint_slow
past LuaJIT's inline budget the gate fires instead of the regression
landing silently in shapes_bench. Total gate count: 19 -> 23.

Makefile gains `bench-wire` and `bench-shapes` phony targets.
6fb4b04c — Eugene Blikh 3 months ago
bench: lazy decode/encode scenarios + jit-trace coverage

Adds bench/lazy_bench.lua comparing eager decode/encode against
decode_lazy:encode across three workloads (passthrough, sparse-read,
mutate-then-reencode) at 1KB/10KB/100KB on emails-heavy Person.
Output is stderr-only (varies with CPU load; not committed to
baseline.json).

Findings (LuaJIT 2.1.0-beta3, both modes):
  - Passthrough re-encode: lazy 1.0–1.5× faster. Untouched views skip
    field-walk entirely and return their original bytes verbatim.
  - Sparse :get x2: lazy 0.60–0.77× of eager. Per-segment Lua tables
    allocated during the index pass cancel the decode-skip savings on
    this flat shape (no large subtrees to skip_field over).
  - Mutate-then-reencode: lazy 0.81–1.07× of eager — roughly
    break-even. Both paths traverse the full byte range; lazy
    splices, eager re-emits per field.

Lazy is a byte-passthrough optimization, not a universal speedup. Use
it when you decode, touch few fields, and re-encode — the proxy /
router shape.

Also extends bench/jit_trace.lua with three lazy hot-path checks:
index pass, sparse :get x2, and untouched :encode. All compile with
no fatal aborts (19/19 trace-stability checks pass; one harmless
side-trace bridge per scenario at lazy.lua's index loop, same pattern
as the existing decode_varint bridge).
aff3ee42 — Eugene Blikh 3 months ago
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.