DramaticShapeVoxelMod archival mirror · read-only
tests/stadium_budget_test.lua

stadium_budget_test.lua

tests/stadium_budget_test.lua5.1 KB10064410c5f90a24
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")