~bigbes/tarantool

tarantool-protobuf

5d0e723e5907b2b93c8e6669e6aeb48cf0c4898c — Eugene Blikh 3 months ago 4b91822
bench: starwing cross-runtime comparison harness

Adds bench/starwing_bench.lua, a sibling to bench/bench.lua that
runs the same Person and BenchPayload payloads through Starwing's
lua-protobuf (installed as a rock at .rocks/lib/tarantool/pb.so)
instead of our pure-Lua pb runtime. Same payload shapes, same
iteration counts, same measurement loop — so the two outputs
line up column-for-column.

Loading approach: strip runtime/ off package.path so the `pb`
module name resolves to starwing's C module. We use
package.loadlib to grab `luaopen_pb` from the .so and inject
into package.loaded before require('pb') has a chance to hit the
Lua-path loader; starwing's pb.option('int64_as_number') matches
the regime our bench/bench.lua exercises (bench payloads stay in
the Lua number range, avoiding per-call cdata allocation noise).

bench/starwing/{hello.pb,proto2_basic.pb} are the FileDescriptor
binaries starwing's pb.load() consumes; produced by mainline
protoc --descriptor_set_out from the same .proto sources we
generate Lua from.

Mirrors bench/go/'s role for Go (`bench: Go cross-runtime
comparison harness`, 07eef11) but for the in-Tarantool C-module
alternative — answers "how do we compare to the C-module
incumbent on the same host" rather than "how do we compare to
mainline C++/Go on a different host."
3 files changed, 244 insertions(+), 0 deletions(-)

A bench/starwing/hello.pb
A bench/starwing/proto2_basic.pb
A bench/starwing_bench.lua
A bench/starwing/hello.pb => bench/starwing/hello.pb +0 -0
A bench/starwing/proto2_basic.pb => bench/starwing/proto2_basic.pb +21 -0
@@ 0,0 1,21 @@

�
proto2_basic.protoproto2_basic"�
BenchPayload
id (Rid
name (	:	anonymousRname
retries (:3Rretries'

lucky_numbers (BRluckyNumbers
tags (	Rtags6
inner (2 .proto2_basic.BenchPayload.InnerRinner6
stats (
2 .proto2_basic.BenchPayload.StatsRstatsB
Stats

latency_ns (R	latencyNs
attempts	 (Rattempts1
Inner
key (	Rkey
weight (Rweight*d�:7
	ext_count.proto2_basic.BenchPayloadd (RextCount:7
	ext_label.proto2_basic.BenchPayloade (	RextLabelB=Z;github.com/tarantool-protobuf/bench/go/pb/proto2pb;proto2pb
\ No newline at end of file

A bench/starwing_bench.lua => bench/starwing_bench.lua +223 -0
@@ 0,0 1,223 @@
#!/usr/bin/env tarantool
-- Head-to-head bench: starwing/lua-protobuf vs this repo's pb runtime.
--
-- Mirrors bench/bench.lua's payload shapes, sizes, iteration counts, and
-- measurement loop so the numbers line up column-for-column. Runs ONLY
-- the starwing path; our path stays in bench/bench.lua and we put the
-- two outputs side-by-side.
--
-- We strip our runtime/ off package.path so `require('pb')` resolves to
-- starwing's pb.so (installed under .rocks/lib/tarantool/) — same module
-- name, different code.

-- Resolve `pb` to starwing's C module, not our Lua runtime that's
-- installed as a rock under .rocks/share/tarantool/pb/. We do this by
-- loading the .so directly through package.loadlib and stuffing the
-- result into package.loaded before require('pb') has a chance to hit
-- the Lua-path loader.
local clock = require('clock')
local fio   = require('fio')

local so_path = '.rocks/lib/tarantool/pb.so'
assert(fio.path.exists(so_path), 'starwing pb.so not at ' .. so_path)
local loader = assert(package.loadlib(so_path, 'luaopen_pb'))
package.loaded.pb = loader()
local pb = require('pb')      -- starwing
local io_   = io

-- ---- schema load -----------------------------------------------------------

local function slurp(path)
    local f = assert(fio.open(path, {'O_RDONLY'}))
    local s = f:read()
    f:close()
    return s
end

assert(pb.load(slurp('bench/starwing/hello.pb')),
    'failed to load hello.pb')
assert(pb.load(slurp('bench/starwing/proto2_basic.pb')),
    'failed to load proto2_basic.pb')

-- Match our runtime's default: int64 as cdata (starwing calls this 'int64').
pb.option('int64_as_number')   -- bench payloads stay in number range; avoids
                               -- per-call cdata allocation noise. Same regime
                               -- our bench/bench.lua exercises.

-- ---- payload builders (verbatim from bench/bench.lua) ----------------------

local function build_person_payload(target)
    if target <= 10 then
        return {name = 'bigbes', age = 42}
    end
    if target <= 100 then
        return {
            name  = string.rep('a', target - 10),
            age   = 42,
        }
    end
    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 build_proto2_payload(target)
    if target <= 0 then
        -- starwing accepts extensions inline as fields of the .pb type
        -- (it loads the descriptor set, which includes them). The
        -- registered extension names are short — see registry below.
        return {id = 7, ext_count = 42}
    end
    local n_tags    = 16
    local per_tag   = 30
    local tags      = {}
    for i = 1, n_tags do
        tags[i] = string.rep('t', per_tag - 2) .. string.format('%02d', i)
    end
    local lucky = {}
    for i = 1, 8 do lucky[i] = 1000 + i end
    return {
        id            = 7,
        name          = 'bench',
        retries       = 9,
        lucky_numbers = lucky,
        tags          = tags,
        inner         = {key = string.rep('k', 16), weight = 3},
        Stats         = {latency_ns = 1234567, attempts = 4},
        ext_count     = 99,
        ext_label     = string.rep('x', 32),
    }
end

-- ---- measurement (mirrors bench/bench.lua) ---------------------------------

local PERSON_SIZES = {
    {label = '10B',   target = 10},
    {label = '100B',  target = 100},
    {label = '1KB',   target = 1024},
    {label = '10KB',  target = 10240},
    {label = '100KB', target = 102400},
}
local PROTO2_SIZES = {
    {label = 'min',  target = 0},
    {label = 'mid',  target = 1024},
}

local function iter_count(size_bytes)
    if size_bytes <  100   then return 200000 end
    if size_bytes <  2000  then return  50000 end
    if size_bytes <  20000 then return   5000 end
    return 500
end

local function summarize(samples)
    table.sort(samples)
    local n = #samples
    return {
        median = samples[math.floor((n + 1) / 2)],
        min    = samples[1],
        max    = samples[n],
    }
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_throughput(fn, n, runs)
    for _ = 1, math.min(n, 1000) do fn() end
    local times = {}
    for r = 1, runs do
        collectgarbage('collect')
        times[r] = time_loop(fn, n)
    end
    local s = summarize(times)
    return {
        ns_per_op   = s.median / n * 1e9,
        msgs_per_s  = n / s.median,
    }
end

local function bench_alloc(fn, expected_bytes)
    local budget   = 64 * 1024 * 1024
    local per_iter = math.max(1, expected_bytes) * 2
    local n        = math.max(100, math.min(2000, math.floor(budget / per_iter)))
    for _ = 1, 100 do fn() end
    collectgarbage('collect')
    collectgarbage('stop')
    local before = collectgarbage('count')
    for _ = 1, n do fn() end
    local after = collectgarbage('count')
    collectgarbage('restart')
    collectgarbage('collect')
    return {
        bytes_per_op = (after - before) * 1024 / n,
    }
end

local function bench_one(type_name, payload)
    local encode = function() return pb.encode(type_name, payload) end
    local bytes  = pb.encode(type_name, payload)
    local decode = function() return pb.decode(type_name, bytes) end

    -- warmup decode
    local _ = pb.decode(type_name, bytes)

    local n    = iter_count(#bytes)
    local runs = 5

    local enc = bench_throughput(encode, n, runs)
    local ea  = bench_alloc(encode, #bytes)
    enc.mb_per_s        = #bytes * enc.msgs_per_s / 1e6
    enc.bytes_per_op    = ea.bytes_per_op

    local dec = bench_throughput(decode, n, runs)
    local da  = bench_alloc(decode, #bytes)
    dec.mb_per_s        = #bytes * dec.msgs_per_s / 1e6
    dec.bytes_per_op    = da.bytes_per_op

    return {size_bytes = #bytes, encode = enc, decode = dec}
end

-- ---- run -------------------------------------------------------------------

io_.stderr:write('starwing/lua-protobuf bench\n')
io_.stderr:write(string.format('  version: pb 0.5.3  (tarantool %s)\n', _TARANTOOL))

io_.write('\n=== hello.Person ===\n')
io_.write(string.format('%-7s  %12s  %12s  %12s  %12s\n',
    'size', 'enc MB/s', 'enc B/op', 'dec MB/s', 'dec B/op'))
for _, sz in ipairs(PERSON_SIZES) do
    local p = build_person_payload(sz.target)
    local r = bench_one('hello.Person', p)
    io_.write(string.format('%-7s  %12.2f  %12.1f  %12.2f  %12.1f\n',
        sz.label, r.encode.mb_per_s, r.encode.bytes_per_op,
        r.decode.mb_per_s, r.decode.bytes_per_op))
end

io_.write('\n=== proto2_basic.BenchPayload ===\n')
io_.write(string.format('%-7s  %12s  %12s  %12s  %12s\n',
    'size', 'enc MB/s', 'enc B/op', 'dec MB/s', 'dec B/op'))
for _, sz in ipairs(PROTO2_SIZES) do
    local p = build_proto2_payload(sz.target)
    local r = bench_one('proto2_basic.BenchPayload', p)
    io_.write(string.format('%-7s  %12.2f  %12.1f  %12.2f  %12.1f\n',
        sz.label, r.encode.mb_per_s, r.encode.bytes_per_op,
        r.decode.mb_per_s, r.decode.bytes_per_op))
end

os.exit(0)