#!/usr/bin/env tarantool
-- Surgical alloc probe. Strips the encode path apart, measures KB-delta
-- per primitive operation, so we can attribute the ~136 B/op encode floor
-- to a specific source (table, varint string, result string, ...).
--
-- All cases run with GC stopped so allocations accumulate. We divide by
-- N to get bytes/op. To eliminate string-interning noise we use *unique*
-- input bytes per iteration where appropriate (suffix counter).
package.path = './runtime/?.lua;./runtime/?/init.lua;'
.. './examples/expected/?.lua;./examples/expected/?/init.lua;'
.. package.path
local wire = require('pb.wire')
local hello = require('full.hello.hello_pb')
local N = 50000
local function alloc_per_op(label, fn, mk_arg)
-- Warmup: let the JIT compile and any one-shot allocations settle.
for i = 1, 1000 do fn(mk_arg and mk_arg(i) or nil) end
collectgarbage('collect')
collectgarbage('stop')
local before = collectgarbage('count')
for i = 1, N do fn(mk_arg and mk_arg(i) or nil) end
local after = collectgarbage('count')
collectgarbage('restart')
collectgarbage('collect')
local per_op = (after - before) * 1024 / N
print(string.format(' %-50s %8.1f B/op', label, per_op))
end
print('=== Baseline: noop ===')
alloc_per_op('empty function', function() end)
alloc_per_op('return nil', function() return nil end)
print('\n=== Table allocation ===')
alloc_per_op('local t = {}', function() local t = {} end)
alloc_per_op('local t, n = {}, 0', function() local t, n = {}, 0 end)
alloc_per_op('local t = {}; t[1]=1; t[2]=2; t[3]=3', function()
local t = {}; t[1] = 1; t[2] = 2; t[3] = 3
end)
alloc_per_op('local t = {}; for i=1,5 do t[i]=i end', function()
local t = {}; for i = 1, 5 do t[i] = i end
end)
alloc_per_op('local t = {}; for i=1,10 do t[i]=i end', function()
local t = {}; for i = 1, 10 do t[i] = i end
end)
print('\n=== wire.encode_varint (interned: same input) ===')
alloc_per_op('encode_varint(42) [1-byte fast]', function()
local s = wire.encode_varint(42)
end)
alloc_per_op('encode_varint(200) [2-byte slow]', function()
local s = wire.encode_varint(200)
end)
alloc_per_op('encode_varint(1e6) [3-byte slow]', function()
local s = wire.encode_varint(1000000)
end)
print('\n=== wire.encode_varint (unique per iter) ===')
alloc_per_op('encode_varint(i % 128) [1-byte, unique-ish]', function(i)
local s = wire.encode_varint(i % 128)
end, function(i) return i end)
alloc_per_op('encode_varint(128 + i) [2-byte, unique]', function(i)
local s = wire.encode_varint(128 + i)
end, function(i) return i end)
print('\n=== string.char (intern check) ===')
alloc_per_op('string.char(42) [1-byte, same]', function()
local s = string.char(42)
end)
alloc_per_op('string.char(i % 256) [1-byte, varying]', function(i)
local s = string.char(i % 256)
end, function(i) return i end)
print('\n=== table.concat ===')
alloc_per_op('concat of 3 short literal strings', function()
local s = table.concat({"\x0a", "\x06", "bigbes"})
end)
alloc_per_op('concat of 5 short literal strings', function()
local s = table.concat({"\x0a", "\x06", "bigbes", "\x10", "\x2a"})
end)
print('\n=== Full encode (Person fixtures) ===')
local p10 = {name = 'bigbes', age = 42}
alloc_per_op('Person_encode 10B (same input each iter)', function()
local s = hello.Person_encode(p10)
end)
-- Unique per iter via the lucky_numbers field (varying).
alloc_per_op('Person_encode 10B (varying age field)', function(i)
p10.age = 42 + (i % 100)
local s = hello.Person_encode(p10)
end, function(i) return i end)
local p100 = {name = string.rep('a', 90), age = 42}
alloc_per_op('Person_encode 100B (same input)', function()
local s = hello.Person_encode(p100)
end)
-- Decode floor probe.
local p10_bytes = hello.Person_encode(p10)
local p100_bytes = hello.Person_encode(p100)
print('\n=== Full decode (Person fixtures) ===')
alloc_per_op('Person_decode 10B (same input)', function()
local t = hello.Person_decode(p10_bytes)
end)
alloc_per_op('Person_decode 100B (same input)', function()
local t = hello.Person_decode(p100_bytes)
end)
print('\n=== Drill-down: encode pieces (10B Person) ===')
-- Imitate Person_encode body manually so we can attribute each step.
alloc_per_op('table {} + n', function()
local out, n = {}, 0
end)
alloc_per_op('+ append tag literal', function()
local out, n = {}, 0
n = n + 1; out[n] = "\x0a"
end)
alloc_per_op('+ append name varlen + name string', function()
local out, n = {}, 0
local v = 'bigbes'
n = n + 1; out[n] = "\x0a"
n = n + 1; out[n] = wire.encode_varint(#v)
n = n + 1; out[n] = v
end)
alloc_per_op('+ append age tag + varint', function()
local out, n = {}, 0
local v = 'bigbes'
n = n + 1; out[n] = "\x0a"
n = n + 1; out[n] = wire.encode_varint(#v)
n = n + 1; out[n] = v
n = n + 1; out[n] = "\x10"
n = n + 1; out[n] = wire.encode_varint(42)
end)
alloc_per_op('+ final table.concat (full path)', function()
local out, n = {}, 0
local v = 'bigbes'
n = n + 1; out[n] = "\x0a"
n = n + 1; out[n] = wire.encode_varint(#v)
n = n + 1; out[n] = v
n = n + 1; out[n] = "\x10"
n = n + 1; out[n] = wire.encode_varint(42)
local r = table.concat(out)
end)
print('\n=== Large payload encode (unique output to defeat interning) ===')
-- Build a 1KB+ Person; vary an integer field so the encoded output is
-- unique per iter and the result string can't be interned.
local p1k = {
name = 'bigbes', age = 42,
address = {street = '1 Main St', city = 'Springfield', zip = 12345},
lucky_numbers = {7, 13, 21, 42, 99},
emails = {},
}
for i = 1, 26 do p1k.emails[i] = string.rep('e', 28) .. string.format('%04d', i) end
print(string.format(' 1KB Person encoded size = %d bytes', #hello.Person_encode(p1k)))
alloc_per_op('Person_encode 1KB (same input — interned)', function()
local s = hello.Person_encode(p1k)
end)
alloc_per_op('Person_encode 1KB (varying age — NOT interned)', function(i)
p1k.age = 42 + i
local s = hello.Person_encode(p1k)
end, function(i) return i end)