~bigbes/tarantool

tarantool-protobuf

ref: b1d9c7581bcc6b4f299155746dc04acc2330e78c tarantool-protobuf/bench/lazy_bench.lua -rw-r--r-- 4.7 KiB
b1d9c758 — Eugene Blikh test: pin unknown-fields text-format conformance regressions 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#!/usr/bin/env tarantool
-- Lazy-decode micro-benchmark.
--
-- Measures the read+mutate+re-encode workload where lazy decode_lazy
-- (zero-copy index + passthrough on untouched fields) is expected to beat
-- eager decode+encode: large messages where the caller only touches a
-- few fields. Three scenarios:
--
--   1. passthrough: decode_lazy + :encode (no mutation)
--      The strongest claim. Eager has to materialize the whole table
--      and re-walk every field on encode; lazy returns the original
--      bytes verbatim after a single tag-scanning pass.
--
--   2. sparse read: decode_lazy + :get(a) + :get(b)
--      The proxy/router workload — touch a handful of fields out of
--      many. Eager pays for materializing everything, lazy only for
--      the fields read.
--
--   3. one-field rewrite: decode_lazy + :get(a) + :set(b, ...) + :encode
--      The mutation passthrough case. Eager re-encodes every field,
--      lazy splices the original bytes for everything but the dirty one.
--
-- Run:  tarantool bench/lazy_bench.lua
-- Output is human-readable on stderr (varies with CPU load; not committed).

package.path = './runtime/?.lua;./runtime/?/init.lua;'
    .. './examples/expected/?.lua;./examples/expected/?/init.lua;'
    .. package.path

local clock = require('clock')

local SIZES = {
    {label = '1KB',   target = 1024},
    {label = '10KB',  target = 10240},
    {label = '100KB', target = 102400},
}

-- Emails-heavy Person — scales repeated strings to hit target size.
-- Index pass cost ≈ decode cost on this shape: every email has its own
-- tag/len scan, so lazy can't skip-jump over large subtrees. Use this
-- shape to honestly bound the lazy-vs-eager comparison on a workload
-- that gives lazy *no* structural advantage from skip_field.
local function build_payload(target)
    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 iter_count(size_bytes)
    if size_bytes <  2000  then return  50000 end
    if size_bytes <  20000 then return   5000 end
    return 500
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)
    for _ = 1, math.min(n, 1000) do fn() end  -- warmup
    local best = math.huge
    for _ = 1, 5 do
        collectgarbage('collect')
        local t = time_loop(fn, n)
        if t < best then best = t end
    end
    return n / best  -- msgs/s at best-run
end

local function compare(label, eager_fn, lazy_fn, n)
    local e = bench(eager_fn, n)
    local l = bench(lazy_fn,  n)
    local ratio = l / e
    io.stderr:write(string.format(
        '  %-40s  eager %10.0f msgs/s  lazy %10.0f msgs/s  %s%.2fx\n',
        label, e, l, ratio >= 1 and 'lazy ' or 'lazy ', ratio))
end

for _, mode in ipairs({'full', 'runtime'}) do
    io.stderr:write('\n== mode: ' .. mode .. ' ==\n')
    local hello = require(mode .. '.hello.hello_pb')

    for _, size in ipairs(SIZES) do
        io.stderr:write(string.format('\n  size: %s\n', size.label))
        local payload = build_payload(size.target)
        local bytes   = hello.Person_encode(payload)
        local n       = iter_count(#bytes)

        -- 1. passthrough: decode + re-encode with no mutation
        compare('passthrough (decode + reencode)',
            function() hello.Person_encode(hello.Person_decode(bytes)) end,
            function() return hello.Person_decode_lazy(bytes):encode() end,
            n)

        -- 2. sparse read: read 2 top-level fields
        compare('sparse read (name + age)',
            function()
                local t = hello.Person_decode(bytes)
                local _ = t.name; local _2 = t.age
            end,
            function()
                local v = hello.Person_decode_lazy(bytes)
                local _ = v:get('name'); local _2 = v:get('age')
            end,
            n)

        -- 3. one-field rewrite: change `name`, keep everything else
        compare('rewrite name (decode + set + reencode)',
            function()
                local t = hello.Person_decode(bytes)
                t.name = 'mallory'
                local _ = hello.Person_encode(t)
            end,
            function()
                local v = hello.Person_decode_lazy(bytes)
                v:set('name', 'mallory')
                local _ = v:encode()
            end,
            n)
    end
end