#!/usr/bin/env tarantool -- Spike harness for tarantool-protobuf-04c. -- -- Compares pure-Lua `full` mode (current baseline) against the -- hand-written C codec for hello.Person across 5 payload sizes. -- Strategies 2 (per-primitive FFI) and 3 (one generic C call) land -- in follow-up sub-issues. -- -- Usage: -- make -C bench/c_accel -- tarantool bench/c_accel/spike_bench.lua local SCRIPT_DIR = (debug.getinfo(1, 'S').source:match('@?(.*/)') or './') local REPO_ROOT = SCRIPT_DIR .. '../..' package.path = REPO_ROOT .. '/runtime/?.lua;' .. REPO_ROOT .. '/runtime/?/init.lua;' .. REPO_ROOT .. '/examples/expected/?.lua;' .. REPO_ROOT .. '/examples/expected/?/init.lua;' .. package.path package.cpath = SCRIPT_DIR .. '?.dylib;' .. SCRIPT_DIR .. '?.so;' .. package.cpath local clock = require('clock') local full = require('full.hello.hello_pb') local ok, c_person = pcall(require, 'pb_c_person') if not ok then io.stderr:write('failed to load pb_c_person: ' .. tostring(c_person) .. '\n') io.stderr:write('run `make -C bench/c_accel` first\n') os.exit(1) end local ok2, c_generic = pcall(require, 'pb_c_generic') if not ok2 then io.stderr:write('failed to load pb_c_generic: ' .. tostring(c_generic) .. '\n') io.stderr:write('run `make -C bench/c_accel` first\n') os.exit(1) end local ok3, prim_ffi = pcall(require, 'prim_ffi') if not ok3 then io.stderr:write('failed to load prim_ffi: ' .. tostring(prim_ffi) .. '\n') io.stderr:write('run `make -C bench/c_accel` first\n') os.exit(1) end -- Payload builder mirrors bench/bench.lua so numbers are comparable. 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 SIZES = { {label = '10B', target = 10}, {label = '100B', target = 100}, {label = '1KB', target = 1024}, {label = '10KB', target = 10240}, {label = '100KB', target = 102400}, } 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) return samples[math.floor((#samples + 1) / 2)] 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, runs) for _ = 1, math.min(n, 1000) do fn() end local samples = {} for r = 1, runs do collectgarbage('collect') samples[r] = time_loop(fn, n) end return summarize(samples) / n -- seconds per op end -- Sanity: every encoder must emit byte-equal output, every decoder must -- return a table. local function sanity_check() for _, sz in ipairs(SIZES) do local p = build_person_payload(sz.target) local lua_bytes = full.Person_encode(p) for label, mod in pairs({c_person = c_person, c_generic = c_generic, prim_ffi = prim_ffi}) do local bytes = mod.Person_encode(p) if bytes ~= lua_bytes then io.stderr:write(string.format( 'sanity FAIL %s at %s: lua=%d bytes %s=%d bytes\n', label, sz.label, #lua_bytes, label, #bytes)) end local back = mod.Person_decode(lua_bytes) if type(back) ~= 'table' then io.stderr:write(label .. ' decode non-table at ' .. sz.label .. '\n') os.exit(2) end end end end sanity_check() local function fmt_op(t, bytes) return string.format('%8.0f / %7.1f', 1 / t, bytes / t / 1e6) end io.write('hello.Person — pure-Lua (full) vs S2 FFI prims vs S3 generic C vs S4 hand C\n') io.write('×L columns = speedup vs pure-Lua baseline\n\n') io.write(string.format( '%-6s %7s %18s %18s %5s %18s %5s %18s %5s\n', 'size', 'bytes', 'pure-Lua', 'S2 FFI prim', '×L', 'S3 generic C', '×L', 'S4 hand C', '×L')) io.write(string.rep('-', 124) .. '\n') local runs = 5 local function run_phase(phase_name, get_fn) io.write(string.format('\n== %s ==\n', phase_name)) for _, sz in ipairs(SIZES) do local p = build_person_payload(sz.target) local bytes = full.Person_encode(p) local n = iter_count(#bytes) local t_lua = bench(get_fn(full, p, bytes), n, runs) local t_s2 = bench(get_fn(prim_ffi, p, bytes), n, runs) local t_s3 = bench(get_fn(c_generic, p, bytes), n, runs) local t_s4 = bench(get_fn(c_person, p, bytes), n, runs) io.write(string.format( '%-6s %7d %18s %18s %5.2f %18s %5.2f %18s %5.2f\n', sz.label, #bytes, fmt_op(t_lua, #bytes), fmt_op(t_s2, #bytes), t_lua / t_s2, fmt_op(t_s3, #bytes), t_lua / t_s3, fmt_op(t_s4, #bytes), t_lua / t_s4)) end end run_phase('ENCODE', function(mod, p, _) return function() mod.Person_encode(p) end end) run_phase('DECODE', function(mod, _, bytes) return function() mod.Person_decode(bytes) end end) os.exit(0)