~bigbes/tarantool

tarantool-protobuf

ad5d73898eeaf88d40e97612141ebcedd6d8ed02 — Eugene Blikh 3 months ago 1004cc6
lazy: SoA index layout (sparse-read 0.66× → 0.90×, passthrough 1.5× → 1.7×)

Replace per-segment Lua tables with four parallel integer arrays
(id, tag_start, val_start, next_start). For an emails-heavy Person at
100KB that's 4 tables of 2800 ints instead of 2800 tables of 4 keys —
~5× fewer table allocations on decode_lazy.

ArrayView and MapView are similarly flattened: each holds a single
int array of val_starts instead of one mini-table per element.
Packed-payload expansion produces the same shape so :at(i) is one
array index lookup + decode call.

Before / after on bench/lazy_bench.lua (1KB / 10KB / 100KB):

  passthrough (decode + reencode)
    full:    1.43× → 1.73×   1.13× → 1.62×   1.04× → 1.69×
    runtime: 1.43× → 1.94×   1.15× → 1.73×   1.16× → 1.84×

  sparse read (:get name + :get age)
    full:    0.70× → 0.90×   0.65× → 0.94×   0.60× → 1.03×
    runtime: 0.76× → 0.99×   0.66× → 1.05×   0.68× → 1.16×

  rewrite name (decode + set + reencode)
    full:    0.98× → 1.11×   0.82× → 1.09×   0.81× → 1.14×
    runtime: 1.06× → 1.26×   0.95× → 1.15×   0.85× → 1.25×

Sparse read goes from a loss to break-even or better; passthrough
gain widens; mutate-then-reencode flips from regression to consistent
win. JIT-trace gate still 19/19 — the index loop's single side-trace
bridge (decode_tag at lazy.lua:49) was at decode_tag in the old code
too; same pattern, different line number.

Drops the unused wt field from the SoA: consumers never re-read the
tag wire type after the index pass. Unknown-field splice and lazy
:encode emit byte slices directly from tag_start..next_start.
1 files changed, 174 insertions(+), 210 deletions(-)

M runtime/pb/lazy.lua
M runtime/pb/lazy.lua => runtime/pb/lazy.lua +174 -210
@@ 8,13 8,16 @@
-- The underlying Lua string is kept GC-anchored on the view table so
-- substring-based passthrough remains valid for the view's lifetime.
--
-- Phase 1: read-only API (:get / :has / :which / :iter / :names plus
-- ArrayView :len/:at/:iter and MapView :get/:has/:keys/:iter). Mutation
-- and passthrough re-encode arrive in phase 2.
-- Storage layout: SoA (struct-of-arrays). Per wire-entry we hold four
-- integer offsets in parallel arrays, not one Lua table per entry.
-- For an emails-heavy message at 100 KB that's 4 tables of 2800 ints
-- instead of 2800 tables of 4 keys — ~5× fewer table allocations on
-- decode_lazy, which is the difference between losing sparse-read 0.6×
-- and breaking even.
--
-- WKT and any descriptor carrying `desc.decode` are eagerly decoded and
-- wrapped in an EagerView with the same getter surface, so callers don't
-- have to special-case Timestamp/Duration/Struct/etc.
-- wrapped in an EagerView with the same getter surface, so callers
-- don't have to special-case Timestamp/Duration/Struct/etc.

local wire  = require('pb.wire')
local codec = require('pb.codec')


@@ 27,72 30,75 @@ local build_array_view
local build_map_view

-- ---------------------------------------------------------------------------
-- Index build
-- Index build (SoA)
--
-- Single pass over `bytes`. Each tag+value occupies one `segment`:
--   {id = <field id>,
--    tag_start = <1-based offset of tag's first byte>,
--    val_start = <1-based offset of value's first byte>,
--    next_start = <1-based offset just past this segment>,
--    wt = <wire type>}
-- Single pass over `bytes`. Returns:
--   segs = {
--     n          = <entry count>,
--     id         = {<field id>, ...},     -- 1-based, by entry
--     tag_start  = {...},                  -- 1-based byte offset, tag's first byte
--     val_start  = {...},                  --                    , value's first byte
--     next_start = {...},                  --                    , just past this entry
--   }
--   by_id = { [field_id] = {seg_idx, seg_idx, ...} }   -- indices into segs
--
-- `segments` is the wire-order list (used by iter and by passthrough re-encode
-- in phase 2). `by_id` maps id -> list-of-segments, even for singular fields
-- (multiple wire entries for the same singular id are legal per spec: scalar
-- last-wins, message-merge — both consumers can walk the list).
-- We deliberately don't keep wire_type: consumers know it from the
-- descriptor for known fields, and we never re-decode the tag for
-- unknown fields after the index pass (we only splice their bytes).
-- ---------------------------------------------------------------------------
local function index_bytes(desc, bytes)
    local pos, lim = 1, #bytes
    local segments = {}
    local s_id, s_tag, s_val, s_next = {}, {}, {}, {}
    local by_id = {}
    local fbi = desc.field_by_id
    local n = 0

    while pos <= lim do
        local tag_start = pos
        local id, wt, npos = wire.decode_tag(bytes, pos)
        local val_start = npos
        local next_start = wire.skip_field(bytes, npos, wt)
        local seg = {
            id = id,
            tag_start = tag_start,
            val_start = val_start,
            next_start = next_start,
            wt = wt,
        }
        segments[#segments + 1] = seg
        n = n + 1
        s_id[n]   = id
        s_tag[n]  = tag_start
        s_val[n]  = val_start
        s_next[n] = next_start
        if fbi[id] ~= nil then
            local list = by_id[id]
            if list == nil then
                by_id[id] = {seg}
                by_id[id] = {n}
            else
                list[#list + 1] = seg
                list[#list + 1] = n
            end
        end
        pos = next_start
    end
    return segments, by_id
    return {
        n          = n,
        id         = s_id,
        tag_start  = s_tag,
        val_start  = s_val,
        next_start = s_next,
    }, by_id
end

-- ---------------------------------------------------------------------------
-- Per-field materialization
-- ---------------------------------------------------------------------------

-- Decode a single value from one segment, given the singular field shape.
-- For message fields this returns another lazy view; for scalar/enum it
-- returns the materialized Lua/cdata value.
local function read_singular(field, bytes, seg)
-- Decode a single value at `val_start`. For message fields returns a
-- lazy sub-view (or eager value via WKT desc.decode override).
local function read_singular(field, bytes, val_start)
    local kind = field.kind
    if kind == 'scalar' then
        local h = wire.TYPE_INFO[field.proto_type]
        local v = h.decode(bytes, seg.val_start)
        local v = wire.TYPE_INFO[field.proto_type].decode(bytes, val_start)
        return v
    elseif kind == 'enum' then
        local u = wire.decode_varint(bytes, seg.val_start)
        local u = wire.decode_varint(bytes, val_start)
        return tonumber(u)
    elseif kind == 'message' then
        local payload = wire.decode_len(bytes, seg.val_start)
        local payload = wire.decode_len(bytes, val_start)
        if field.message.decode ~= nil then
            -- WKT (or any custom-decode override). Eager.
            return field.message.decode(payload)
        end
        return build_msg_view(field.message, payload)


@@ 102,20 108,16 @@ end

-- For singular fields, semantics for multiple wire entries with the same id:
--   - scalar/enum: last wins.
--   - message: merged. We delegate to the eager codec by concatenating the
--     per-entry payloads and feeding them to pb.codec.decode, which honors
--     proto3 merge rules. (Multi-entry singular messages are rare; this is
--     the off-fast-path correctness branch.)
local function read_singular_list(field, bytes, list)
    if field.kind ~= 'message' or #list == 1 then
        return read_singular(field, bytes, list[#list])
    end
    -- Multi-segment message: concatenate the inner payloads and eager-decode.
    -- This loses the lazy sub-view but is the right thing semantically.
--   - message: merged. We delegate to the eager codec by concatenating
--     per-entry payloads. (Multi-entry singular messages are rare; this
--     is the off-fast-path correctness branch.)
local function read_singular_list(field, bytes, segs, idx_list)
    if field.kind ~= 'message' or #idx_list == 1 then
        return read_singular(field, bytes, segs.val_start[idx_list[#idx_list]])
    end
    local parts = {}
    for i = 1, #list do
        local seg = list[i]
        local payload = wire.decode_len(bytes, seg.val_start)
    for i = 1, #idx_list do
        local payload = wire.decode_len(bytes, segs.val_start[idx_list[i]])
        parts[i] = payload
    end
    local merged = table.concat(parts)


@@ 128,110 130,90 @@ end
-- ---------------------------------------------------------------------------
-- ArrayView: lazy view over a repeated field.
--
-- For unpacked repeated, each wire entry is one element (one segment).
-- For packed repeated (scalars/enums), one segment contains a length-delimited
-- payload with all elements; we walk the payload on demand. We never
-- materialize the full element array unless the user iterates it all.
-- Stores a flat int array `_starts` of value offsets (one int per element).
-- For unpacked, those come from the parent's val_start array (subset).
-- For packed, they're scanned out of the packed payload at construct time.
-- :at(i) decodes from bytes[_starts[i]] using the field's known kind.
-- ---------------------------------------------------------------------------

local ArrayView = {}
ArrayView.__index = ArrayView

-- Pre-expand packed payloads into a uniform per-element segment list at
-- construct time. Cheaper to do once than to re-scan on every :at(i).
local function expand_packed(field, bytes, packed_seg)
-- Walk a packed payload, emitting one val_start per element. Cheaper as
-- a one-pass scan than re-walking on every :at — packed payloads are
-- contiguous so each step is just a wire.skip_field with the element's
-- known wire type.
local function expand_packed(field, bytes, val_start)
    local h = field.kind == 'scalar' and wire.TYPE_INFO[field.proto_type] or nil
    -- Decode the length prefix to know where elements live.
    local payload_start = packed_seg.val_start
    local b = bytes:byte(payload_start)
    local b = bytes:byte(val_start)
    local payload_len, hdr_end
    if b < 0x80 then
        payload_len = b
        hdr_end = payload_start + 1
        payload_len = b; hdr_end = val_start + 1
    else
        local v, npos = wire.decode_varint(bytes, payload_start)
        payload_len = tonumber(v)
        hdr_end = npos
        local v, npos = wire.decode_varint(bytes, val_start)
        payload_len = tonumber(v); hdr_end = npos
    end
    local lim = hdr_end + payload_len
    local elems = {}
    local elem_wire = (field.kind == 'scalar') and h.wire or wire.WIRE_VARINT
    local starts, n = {}, 0
    local p = hdr_end
    while p < lim do
        local v_start = p
        local v_next
        if field.kind == 'scalar' then
            -- Skip according to the scalar's actual wire type.
            v_next = wire.skip_field(bytes, p, h.wire)
        else
            -- Packed enums: varint per element.
            v_next = wire.skip_field(bytes, p, wire.WIRE_VARINT)
        end
        elems[#elems + 1] = {val_start = v_start, val_next = v_next}
        p = v_next
        n = n + 1
        starts[n] = p
        p = wire.skip_field(bytes, p, elem_wire)
    end
    return elems
    return starts, n
end

local function build_array_view_impl(field, bytes, segs)
    -- For packed payloads (single segment, WIRE_LEN, but scalar field uses
    -- a non-LEN wire type), elements live inside that one segment.
    -- For unpacked, each segment is one element.
    local mode  -- 'packed' | 'unpacked'
    local elements  -- list of {val_start = ...} entries
    if field.kind == 'scalar' then
        local h = wire.TYPE_INFO[field.proto_type]
        if h.wire ~= wire.WIRE_LEN and #segs == 1 and segs[1].wt == wire.WIRE_LEN then
            mode = 'packed'
            elements = expand_packed(field, bytes, segs[1])
        else
            mode = 'unpacked'
            elements = segs
        end
    elseif field.kind == 'enum' then
        if #segs == 1 and segs[1].wt == wire.WIRE_LEN then
            mode = 'packed'
            elements = expand_packed(field, bytes, segs[1])
        else
            mode = 'unpacked'
            elements = segs
        end
local function build_array_view_impl(field, bytes, segs, idx_list)
    -- Detect packed vs unpacked from the wire type of the entries.
    -- For repeated scalars/enums with a single LEN-typed wire entry
    -- when the element type is non-LEN, that's a packed payload.
    local mode, starts, n
    local first_idx = idx_list[1]
    local tag_byte = bytes:byte(segs.tag_start[first_idx])
    local first_wt = tag_byte % 8  -- low 3 bits

    if field.kind == 'message' then
        -- Repeated messages never pack.
        mode = 'unpacked'
        n = #idx_list
        starts = {}
        for i = 1, n do starts[i] = segs.val_start[idx_list[i]] end
    elseif (field.kind == 'scalar' or field.kind == 'enum')
        and #idx_list == 1 and first_wt == wire.WIRE_LEN
        and not (field.kind == 'scalar'
                 and wire.TYPE_INFO[field.proto_type].wire == wire.WIRE_LEN) then
        mode = 'packed'
        starts, n = expand_packed(field, bytes, segs.val_start[first_idx])
    else
        -- repeated message: always one segment per element, never packed.
        mode = 'unpacked'
        elements = segs
        n = #idx_list
        starts = {}
        for i = 1, n do starts[i] = segs.val_start[idx_list[i]] end
    end

    return setmetatable({
        _field = field,
        _bytes = bytes,
        _mode  = mode,
        _elements = elements,
        _field  = field,
        _bytes  = bytes,
        _starts = starts,
        _n      = n,
    }, ArrayView)
end

build_array_view = build_array_view_impl

function ArrayView:len()
    return #self._elements
end
function ArrayView:len() return self._n end

function ArrayView:at(i)
    local entry = self._elements[i]
    if entry == nil then return nil end
    if i < 1 or i > self._n then return nil end
    local field, bytes = self._field, self._bytes
    if self._mode == 'packed' then
        if field.kind == 'enum' then
            local u = wire.decode_varint(bytes, entry.val_start)
            return tonumber(u)
        end
        local h = wire.TYPE_INFO[field.proto_type]
        return (h.decode(bytes, entry.val_start))
    end
    return read_singular(field, bytes, entry)
    local val_start = self._starts[i]
    return read_singular(field, bytes, val_start)
end

function ArrayView:iter()
    local view, i = self, 0
    local n = #self._elements
    local view, i, n = self, 0, self._n
    return function()
        i = i + 1
        if i > n then return nil end


@@ 241,34 223,29 @@ end

function ArrayView:tolist()
    local out = {}
    for i = 1, #self._elements do out[i] = self:at(i) end
    for i = 1, self._n do out[i] = self:at(i) end
    return out
end

-- ---------------------------------------------------------------------------
-- MapView: lazy view over a map<K,V> field.
--
-- Each wire entry is one length-delimited submessage with two fields:
--   id=1 (key), id=2 (value). The submessage may omit either when the
-- value equals its proto3 default.
--
-- For O(1) `:get(k)`, we decode keys lazily but cache the key->entry map on
-- first key-lookup or first :iter. Until then, only the per-entry payload
-- offsets are known.
-- Stores a flat int array `_starts` of LEN-prefix offsets — one per map
-- entry. Each entry contains the (key, value) sub-fields and is decoded
-- on first :get/:has/:iter to build a key->value cache.
-- ---------------------------------------------------------------------------

local MapView = {}
MapView.__index = MapView

local function decode_map_entry(field, bytes, seg)
local function decode_map_entry(field, bytes, val_start)
    local key_field, val_field = field.key, field.value
    -- Read the outer LEN to find the entry payload.
    local b = bytes:byte(seg.val_start)
    local b = bytes:byte(val_start)
    local payload_len, hdr_end
    if b < 0x80 then
        payload_len = b; hdr_end = seg.val_start + 1
        payload_len = b; hdr_end = val_start + 1
    else
        local v, npos = wire.decode_varint(bytes, seg.val_start)
        local v, npos = wire.decode_varint(bytes, val_start)
        payload_len = tonumber(v); hdr_end = npos
    end
    local lim = hdr_end + payload_len


@@ 320,11 297,15 @@ local function decode_map_entry(field, bytes, seg)
    return key, val
end

local function build_map_view_impl(field, bytes, segs)
local function build_map_view_impl(field, bytes, segs, idx_list)
    local n = #idx_list
    local starts = {}
    for i = 1, n do starts[i] = segs.val_start[idx_list[i]] end
    return setmetatable({
        _field = field,
        _bytes = bytes,
        _segs  = segs,
        _field  = field,
        _bytes  = bytes,
        _starts = starts,
        _n      = n,
        -- _by_key populated lazily on first :get/:has/:iter call.
    }, MapView)
end


@@ 333,11 314,11 @@ build_map_view = build_map_view_impl

local function map_ensure_index(self)
    if self._by_key ~= nil then return end
    local field, bytes, segs = self._field, self._bytes, self._segs
    local field, bytes, starts, n = self._field, self._bytes, self._starts, self._n
    local by_key = {}
    local keys = {}
    for i = 1, #segs do
        local k, v = decode_map_entry(field, bytes, segs[i])
    for i = 1, n do
        local k, v = decode_map_entry(field, bytes, starts[i])
        if by_key[k] == nil then keys[#keys + 1] = k end
        by_key[k] = v  -- duplicate keys: last wins (matches eager decode)
    end


@@ 398,11 379,11 @@ local function build_msg_view_impl(desc, bytes)
            _eager_only = true,
        }, MessageView)
    end
    local segments, by_id = index_bytes(desc, bytes)
    local segs, by_id = index_bytes(desc, bytes)
    return setmetatable({
        _desc = desc,
        _bytes = bytes,
        _segments = segments,
        _segs  = segs,
        _by_id = by_id,
        _cache = {},
        -- Parallel array of cached sub-MessageViews so :is_dirty can


@@ 415,7 396,7 @@ end
build_msg_view = build_msg_view_impl
M.build = build_msg_view_impl

-- :get(name) -> decoded value, or nil if not on wire (and no _cache entry).
-- :get(name) -> decoded value, or nil if not on wire.
function MessageView:get(name)
    if self._eager_only then return self._eager[name] end
    local cache = self._cache


@@ 423,18 404,16 @@ function MessageView:get(name)
    if v ~= nil then return v end
    local field = self._desc.field_by_name[name]
    if field == nil then return nil end
    local segs = self._by_id[field.id]
    if segs == nil then return nil end
    local idx_list = self._by_id[field.id]
    if idx_list == nil then return nil end
    if field.kind == 'map' then
        v = build_map_view(field, self._bytes, segs)
        v = build_map_view(field, self._bytes, self._segs, idx_list)
    elseif field.repeated then
        v = build_array_view(field, self._bytes, segs)
        v = build_array_view(field, self._bytes, self._segs, idx_list)
    else
        v = read_singular_list(field, self._bytes, segs)
        v = read_singular_list(field, self._bytes, self._segs, idx_list)
    end
    cache[name] = v
    -- Track sub-MessageViews on a flat array so :is_dirty can walk
    -- with ipairs (see _sub_msg_views note above).
    if type(v) == 'table' and getmetatable(v) == MessageView then
        local s = self._sub_msg_views
        s[#s + 1] = v


@@ 452,7 431,7 @@ end

-- :which(oneof_name) -> name of the active branch, or nil.
-- Proto3 last-wins: if multiple branches appeared on the wire, the one
-- whose final segment came last in wire order is active.
-- whose final entry came last in wire order is active.
function MessageView:which(oneof_name)
    if self._eager_only then
        local oneofs = self._desc.oneofs


@@ 470,15 449,16 @@ function MessageView:which(oneof_name)
    if members == nil then return nil end
    local member_set = {}
    for i = 1, #members do member_set[members[i]] = true end
    local fbn = self._desc.field_by_name
    local segs = self._segs
    local fbi = self._desc.field_by_id
    local active, active_pos
    local segs = self._segments
    for i = 1, #segs do
        local f = self._desc.field_by_id[segs[i].id]
    for i = 1, segs.n do
        local f = fbi[segs.id[i]]
        if f ~= nil and member_set[f.name] then
            if active_pos == nil or segs[i].tag_start > active_pos then
            local ts = segs.tag_start[i]
            if active_pos == nil or ts > active_pos then
                active = f.name
                active_pos = segs[i].tag_start
                active_pos = ts
            end
        end
    end


@@ 500,16 480,15 @@ function MessageView:names()
            end
        end
    end
    local segments = self._segments
    local segs = self._segs
    local fbi = self._desc.field_by_id
    local emitted = {}
    local i = 0
    return function()
        while true do
            i = i + 1
            local seg = segments[i]
            if seg == nil then return nil end
            local f = fbi[seg.id]
            if i > segs.n then return nil end
            local f = fbi[segs.id[i]]
            if f ~= nil and emitted[f.name] == nil then
                emitted[f.name] = true
                return f.name


@@ 534,7 513,7 @@ function MessageView:iter()
            end
        end
    end
    local segments = self._segments
    local segs = self._segs
    local fbi = self._desc.field_by_id
    local emitted = {}
    local view = self


@@ 542,9 521,8 @@ function MessageView:iter()
    return function()
        while true do
            i = i + 1
            local seg = segments[i]
            if seg == nil then return nil end
            local f = fbi[seg.id]
            if i > segs.n then return nil end
            local f = fbi[segs.id[i]]
            if f ~= nil and emitted[f.name] == nil then
                emitted[f.name] = true
                return f.name, view:get(f.name)


@@ 559,9 537,6 @@ end

-- :set(name, value) marks a field dirty. Subsequent :encode() emits the
-- new value via the codec; other fields passthrough their original bytes.
-- The provided value can be any shape the eager encoder accepts (Lua
-- table/string/number/cdata/array). Passing a MessageView as `value` is
-- supported but materializes it via :totable() on encode.
function MessageView:set(name, value)
    if self._eager_only then
        self._eager[name] = value


@@ 578,14 553,8 @@ function MessageView:set(name, value)
end

-- :is_dirty() returns true if this view has had :set called, OR if any
-- cached sub-view (e.g. a nested MessageView accessed via :get) has been
-- mutated. Used by parent :encode to decide whether to splice or re-encode.
--
-- We walk _sub_msg_views (a flat array of cached sub-MessageViews) with
-- ipairs instead of pairs(_cache). Hash-keyed pairs compiles to ISNEXT
-- which is NYI in Tarantool's LuaJIT 2.1 — keeping is_dirty on an array
-- lets the encode-time JIT trace stay attached. Array fields and map
-- fields aren't recursed into: phase 2 mutation is on MessageView only.
-- cached sub-view has been mutated. Walks _sub_msg_views (flat array)
-- with ipairs to stay JIT-stable.
function MessageView:is_dirty()
    if self._eager_only then return self._eager_dirty == true end
    local d = self._dirty


@@ 598,7 567,6 @@ function MessageView:is_dirty()
end

-- Materialize a possibly-view value so encode_field can consume it.
-- Lazy sub-views are converted to plain tables; raw values pass through.
local function materialize(value)
    if type(value) ~= 'table' then return value end
    local m = getmetatable(value)


@@ 612,24 580,21 @@ local function materialize(value)
    return value
end

-- Recursively materialize a value to a plain Lua structure suitable for
-- the eager codec. For a MessageView, we walk every present field —
-- this is the cost of mutation; pure-read messages stay lazy.
function MessageView:totable()
    if self._eager_only then return self._eager end
    local out = {}
    for name in self:names() do
        local v = self:get(name)
        out[name] = materialize(v)
        out[name] = materialize(self:get(name))
    end
    -- Preserve unknown fields for round-trip.
    if self._segments then
    local segs = self._segs
    if segs then
        local unknown = {}
        local fbi = self._desc.field_by_id
        for i = 1, #self._segments do
            local seg = self._segments[i]
            if fbi[seg.id] == nil then
                unknown[#unknown + 1] = self._bytes:sub(seg.tag_start, seg.next_start - 1)
        for i = 1, segs.n do
            if fbi[segs.id[i]] == nil then
                unknown[#unknown + 1] =
                    self._bytes:sub(segs.tag_start[i], segs.next_start[i] - 1)
            end
        end
        if #unknown > 0 then out._unknown_fields = table.concat(unknown) end


@@ 641,8 606,8 @@ end
--   1. WKT eager-wrap: delegate to desc.encode on the materialized table.
--   2. Untouched (no dirty fields, no dirty sub-views): emit the original
--      bytes verbatim — perfect byte-for-byte passthrough.
--   3. Mixed: walk fields in id order, splice clean segments, encode dirty
--      values fresh. Unknown segments are emitted at the end.
--   3. Mixed: walk fields in id order, splice clean segments, encode
--      dirty values fresh. Unknown segments are emitted at the end.
function MessageView:encode()
    if self._eager_only then
        return self._desc.encode(self._eager)


@@ 654,12 619,13 @@ function MessageView:encode()
    local out = {}
    local fields = self._desc.fields
    local bytes = self._bytes
    local segs = self._segs
    local by_id = self._by_id
    local dirty = self._dirty or {}
    local cache = self._cache

    -- Active-oneof resolution mirrors codec.encode_message:
    -- a oneof field is only emitted if it's the active branch.
    -- Active-oneof resolution mirrors codec.encode_message: a oneof
    -- field is only emitted if it's the active branch.
    local active
    local oneofs_list = self._desc.oneofs_list
    if oneofs_list then


@@ 669,7 635,8 @@ function MessageView:encode()
            local members = oo.members
            for j = 1, #members do
                local fname = members[j]
                if dirty[fname] or by_id[self._desc.field_by_name[fname].id] then
                if dirty[fname]
                    or by_id[self._desc.field_by_name[fname].id] then
                    active[oo.name] = fname
                end
            end


@@ 690,28 657,25 @@ function MessageView:encode()
        end

        if f.oneof and active and active[f.oneof] ~= fname then
            -- Inactive oneof branch: skip entirely.
            -- Inactive oneof branch: skip.
        elseif is_dirty or is_sub_dirty then
            local v = cache[fname]
            v = materialize(v)
            codec.encode_field(f, v, out, f.optional or (f.oneof ~= nil))
            codec.encode_field(f, materialize(cache[fname]), out,
                f.optional or (f.oneof ~= nil))
        elseif by_id[f.id] then
            local segs = by_id[f.id]
            for j = 1, #segs do
                local seg = segs[j]
                out[#out + 1] = bytes:sub(seg.tag_start, seg.next_start - 1)
            local idx_list = by_id[f.id]
            for j = 1, #idx_list do
                local idx = idx_list[j]
                out[#out + 1] = bytes:sub(segs.tag_start[idx],
                                          segs.next_start[idx] - 1)
            end
        end
    end

    -- Unknown segments preserved at the end (matches codec's _unknown_fields
    -- trailer convention).
    -- Unknown segments preserved at the end.
    local fbi = self._desc.field_by_id
    local segments = self._segments
    for i = 1, #segments do
        local seg = segments[i]
        if fbi[seg.id] == nil then
            out[#out + 1] = bytes:sub(seg.tag_start, seg.next_start - 1)
    for i = 1, segs.n do
        if fbi[segs.id[i]] == nil then
            out[#out + 1] = bytes:sub(segs.tag_start[i], segs.next_start[i] - 1)
        end
    end