mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 12:50:50 +02:00
150 lines
5.1 KiB
Lua
150 lines
5.1 KiB
Lua
-- 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")
|