#!/usr/bin/env tarantool -- Lazy-decode micro-benchmark. -- -- Measures the read+mutate+re-encode workload where lazy decode_lazy -- (zero-copy index + passthrough on untouched fields) is expected to beat -- eager decode+encode: large messages where the caller only touches a -- few fields. Three scenarios: -- -- 1. passthrough: decode_lazy + :encode (no mutation) -- The strongest claim. Eager has to materialize the whole table -- and re-walk every field on encode; lazy returns the original -- bytes verbatim after a single tag-scanning pass. -- -- 2. sparse read: decode_lazy + :get(a) + :get(b) -- The proxy/router workload — touch a handful of fields out of -- many. Eager pays for materializing everything, lazy only for -- the fields read. -- -- 3. one-field rewrite: decode_lazy + :get(a) + :set(b, ...) + :encode -- The mutation passthrough case. Eager re-encodes every field, -- lazy splices the original bytes for everything but the dirty one. -- -- Run: tarantool bench/lazy_bench.lua -- Output is human-readable on stderr (varies with CPU load; not committed). package.path = './runtime/?.lua;./runtime/?/init.lua;' .. './examples/expected/?.lua;./examples/expected/?/init.lua;' .. package.path local clock = require('clock') local SIZES = { {label = '1KB', target = 1024}, {label = '10KB', target = 10240}, {label = '100KB', target = 102400}, } -- Emails-heavy Person — scales repeated strings to hit target size. -- Index pass cost ≈ decode cost on this shape: every email has its own -- tag/len scan, so lazy can't skip-jump over large subtrees. Use this -- shape to honestly bound the lazy-vs-eager comparison on a workload -- that gives lazy *no* structural advantage from skip_field. local function build_payload(target) local per_email = 36 local fixed_bytes = 80 local n_emails = math.max(1, math.floor((target - fixed_bytes) / per_email)) local p = { name = 'bigbes', age = 42, address = {street = '1 Main St', city = 'Springfield', zip = 12345}, lucky_numbers = {7, 13, 21, 42, 99}, emails = {}, } for i = 1, n_emails do p.emails[i] = string.rep('e', 28) .. string.format('%04d', i) end return p end local function iter_count(size_bytes) if size_bytes < 2000 then return 50000 end if size_bytes < 20000 then return 5000 end return 500 end local function time_loop(fn, n) local t0 = clock.monotonic64() for _ = 1, n do fn() end local t1 = clock.monotonic64() return tonumber(t1 - t0) / 1e9 end local function bench(fn, n) for _ = 1, math.min(n, 1000) do fn() end -- warmup local best = math.huge for _ = 1, 5 do collectgarbage('collect') local t = time_loop(fn, n) if t < best then best = t end end return n / best -- msgs/s at best-run end local function compare(label, eager_fn, lazy_fn, n) local e = bench(eager_fn, n) local l = bench(lazy_fn, n) local ratio = l / e io.stderr:write(string.format( ' %-40s eager %10.0f msgs/s lazy %10.0f msgs/s %s%.2fx\n', label, e, l, ratio >= 1 and 'lazy ' or 'lazy ', ratio)) end for _, mode in ipairs({'full', 'runtime'}) do io.stderr:write('\n== mode: ' .. mode .. ' ==\n') local hello = require(mode .. '.hello.hello_pb') for _, size in ipairs(SIZES) do io.stderr:write(string.format('\n size: %s\n', size.label)) local payload = build_payload(size.target) local bytes = hello.Person_encode(payload) local n = iter_count(#bytes) -- 1. passthrough: decode + re-encode with no mutation compare('passthrough (decode + reencode)', function() hello.Person_encode(hello.Person_decode(bytes)) end, function() return hello.Person_decode_lazy(bytes):encode() end, n) -- 2. sparse read: read 2 top-level fields compare('sparse read (name + age)', function() local t = hello.Person_decode(bytes) local _ = t.name; local _2 = t.age end, function() local v = hello.Person_decode_lazy(bytes) local _ = v:get('name'); local _2 = v:get('age') end, n) -- 3. one-field rewrite: change `name`, keep everything else compare('rewrite name (decode + set + reencode)', function() local t = hello.Person_decode(bytes) t.name = 'mallory' local _ = hello.Person_encode(t) end, function() local v = hello.Person_decode_lazy(bytes) v:set('name', 'mallory') local _ = v:encode() end, n) end end