@@ 0,0 1,167 @@
+#!/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)