stadium_budget_test.lua
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
-- What the model build COSTS -- the numbers that decide whether it can run -- on a phone. -- -- luajit mods/DramaticShapeVoxelMod/tests/stadium_budget_test.lua [--rom=PATH] -- -- Run from the PROJECT ROOT. -- -- ------- why this is a test and not a note -- -- The extraction is pure Lua on purpose -- no FFI, no native helper, no -- second process -- specifically so it can run everywhere LOVE does, phones -- included. That claim is about MEMORY as much as about which functions -- exist: a desktop will not notice a transient peak that would have a mobile -- OS kill the process, and the peak is not visible by reading the code. -- -- So this measures it, and fails if the two things that must stay bounded -- stop being bounded: -- -- the WORKING SET must not grow with the number of species done, or the -- build gets heavier the longer it runs and dies somewhere in the 140s -- -- the PEAK must stay under a budget a phone can be expected to have -- spare, on top of a game that is already running -- -- The ROM itself is most of the floor and cannot be avoided -- the archive is -- addressed by absolute offset -- so it is reported separately from the -- per-species work built on top of it. local args = {} for _, a in ipairs({ ... }) do local k, v = a:match("^%-%-([%w_]+)=(.*)$") if k then args[k] = v end end local MOD = "mods/DramaticShapeVoxelMod" local ROM = args.rom or (MOD .. "/model_extract/baseroms/us/baserom.z64") -- How much headroom the whole build may take on top of the ROM, in MB. Sized -- against the smallest thing this is expected to run on rather than against -- what is convenient: a mid-range phone from several years ago, with a game -- already resident. local PEAK_BUDGET_MB = 220 -- How much the working set may drift between the first ten species and the -- last ten, in MB. Not zero -- species differ in size by a factor of ten, and -- the collector is not obliged to run on any particular schedule -- but a -- genuine leak shows up here as tens of megabytes. local DRIFT_BUDGET_MB = 24 local loaded = {} local V = {} function V.require(name) if loaded[name] == nil then local chunk = assert(loadfile(MOD .. "/lib/" .. name .. ".lua")) loaded[name] = chunk(V) end return loaded[name] end V.mod = { log = { warn = function() end, info = function() end } } local StadiumRom = V.require("StadiumRom") local StadiumBuild = V.require("StadiumBuild") local function mb() return collectgarbage("count") / 1024 end -- A single collectgarbage() is not guaranteed to finish a cycle, and a -- half-finished one reads tens of megabytes high -- which showed up here as -- "settled" figures BELOW the baseline they were measured against. Run it -- until the number stops moving. local function settle() local last = mb() for _ = 1, 8 do collectgarbage("collect") local now = mb() if now >= last - 0.05 then return now end last = now end return mb() end -- The baseline is taken BEFORE the ROM is read, or the file is already on the -- heap when it is measured and the ROM appears to cost nothing. local base = settle() local fp = io.open(ROM, "rb") if not fp then io.stderr:write("no ROM at " .. ROM .. "\n") os.exit(2) end local romBytes = fp:read("*a") fp:close() local rom = assert(StadiumRom.open(romBytes)) local withRom = settle() local peak, settled = withRom, {} local bytes = 0 local t0 = os.clock() for fileno = 0, StadiumRom.N_POKEMON - 1 do local res = assert(StadiumBuild.species(rom, fileno)) bytes = bytes + #res.bytes -- the live peak, before the collector has been asked for anything: this is -- what the allocator actually had to find local live = mb() if live > peak then peak = live end -- and the settled working set, which is what must not creep settled[fileno + 1] = settle() end local dt = os.clock() - t0 local function avg(from, to) local sum = 0 for i = from, to do sum = sum + settled[i] end return sum / (to - from + 1) end local first = avg(1, 10) local last = avg(#settled - 9, #settled) local drift = last - first io.write(("ROM %6.1f MB (the file, held for the whole build)\n") :format(withRom - base)) io.write(("peak working set %6.1f MB (over the ROM)\n"):format(peak - withRom)) io.write(("settled, first 10 %6.1f MB\n"):format(first - withRom)) io.write(("settled, last 10 %6.1f MB\n"):format(last - withRom)) io.write(("drift %+6.1f MB (budget %.0f)\n") :format(drift, DRIFT_BUDGET_MB)) io.write(("total peak %6.1f MB (budget %.0f)\n") :format(peak, PEAK_BUDGET_MB)) io.write(("output %6.1f MB in %.1fs (%.0f ms a species)\n") :format(bytes / 1e6, dt, dt * 1000 / StadiumRom.N_POKEMON)) local fail = false if peak > PEAK_BUDGET_MB then io.write(("FAIL peak %.1f MB is over the %.0f MB budget\n") :format(peak, PEAK_BUDGET_MB)) fail = true end if drift > DRIFT_BUDGET_MB then io.write(("FAIL the working set grew %.1f MB across the run -- something is " .. "being retained per species\n"):format(drift)) fail = true end if fail then os.exit(1) end io.write("PASS -- bounded working set, peak within budget\n")