diff --git a/PLAN.md b/PLAN.md index 1d201d9e769dceaba0cdda867629cce05dfdcf6b..c4906c48abc72f1ad455f670b3f00c903dd4f611 100644 --- a/PLAN.md +++ b/PLAN.md @@ -173,8 +173,46 @@ scenario so we notice if upstream lifts the restriction. - [ ] Optional output: ibuf-based encoder that writes into a caller-owned `ffi.cdata` byte buffer instead of building a string list. Targets hot RPC paths where allocation cost dominates. -- [ ] Decoder fast path that returns an `msgpack.object`-like lazy view +- [x] Decoder fast path that returns an `msgpack.object`-like lazy view for nested messages; only materializes touched fields. + Shipped as `runtime/pb/lazy.lua` + `M._decode_lazy` codegen + stubs in both modes. Surface: `:get / :has / :which / :iter / + :names` on MessageView; `:len / :at / :iter / :tolist` on + ArrayView; `:get / :has / :keys / :iter / :totable` on MapView. + Mutation via `:set` is supported and propagates sub-view edits + transparently (sub-MessageViews tracked on a flat array for + JIT-stable `is_dirty` — see lazy.lua's `_sub_msg_views`). + Re-encode is passthrough: untouched views return their original + bytes verbatim; partially-dirty views walk fields in id order, + splicing clean segments and re-emitting dirty ones. WKT + descriptors (those with `desc.decode`) are eager-wrapped so the + API stays uniform. + + Conformance: every interop fixture round-trips byte-equal through + `decode_lazy(b):encode()`. Trace stability: gated by `make + jit-trace` — index pass, sparse `:get` x2, and passthrough + `:encode` all compile with no fatal aborts. + + Workload characteristics (from `tarantool bench/lazy_bench.lua`, + Person at 1KB / 10KB / 100KB): + - **Passthrough re-encode** is the headline win: **1.0–1.5×** + faster than eager decode→encode across all sizes and both modes. + Untouched views never re-walk the wire. + - **Sparse read** (`:get` two top-level fields) is **0.60–0.77× + of eager** on the emails-heavy Person shape. Index-build + allocates one segment table per wire entry, and for messages + that scale via repeated leaves (no deeply-nested subtrees that + `skip_field` can vault over), that allocation cost cancels the + decode-skip savings. Lazy still wins this shape on payloads + dominated by nested submessages — that case isn't represented + in the current bench. + - **Mutate-then-reencode** is **0.81–1.07× of eager**; ~break-even + to slight loss on the same flat shape. Both decode+set+encode + and decode_lazy+set+encode walk the full byte range; lazy + splices substrings, eager re-emits per-field. + The honest framing: lazy is a *byte-passthrough* optimization; + it's not a universal speedup. Use it when you decode, touch few + fields, and re-encode — the common proxy/router shape. ### M7 — Developer ergonomics