From 1004cc60cd4f3912be5360c99c3cf026f41229c0 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Fri, 15 May 2026 17:18:35 +0300 Subject: [PATCH] docs: PLAN.md M6 lazy view entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marks the M6 "lazy view" bullet done with the API surface, conformance claim (interop byte-equal through decode_lazy:encode), trace stability (make jit-trace 19/19), and the honest perf characteristic from bench/lazy_bench.lua — lazy wins passthrough re-encode (1.0–1.5×), loses sparse-read 0.60–0.77× on the flat shape because per-segment table allocation dominates index cost. Mutate-then-reencode is roughly break-even. Lazy is a byte-passthrough optimization, not a universal speedup. --- PLAN.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/PLAN.md b/PLAN.md index 1d201d9e769dceaba0cdda867629cce05dfdcf6b..c4906c48abc72f1ad455f670b3f00c903dd4f611 100644 --- a/PLAN.md +++ b/PLAN.md @@ -173,8 +173,46 @@ fiber and bridges client ↔ handler via `fiber.channel`. All four flavors - [ ] 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