ÎÌÌÌ̈ˆˆ¬ªªª¬ˆˆˆ¬ÌÌ̬ÌÀ̬ÌÀ̬ÌÀÌÌÌìÌÀÀî̬ÀÌÀ¬ÀÀÀ¬ÀÀÀ¬ÀÀÎÌÌÌ̈ˆˆ¬ªªª¬ˆˆˆ¬ÌÌ̬ÌÌ̬ÌÀ̬ÌÀÌÌÌìÌÀÀî̬ÀÌ̬ÀÀÀ¬ÀÀÀ¬ÀÀ¬ÌÌ̬ªªª¬ª¬ª¬ªÊ̬ªªªŒˆˆˆÌÀÌÎÌÌÎ̬̪ªÀ쬪ÀÀÀÌîÀìîÌÌîî¬ÌÌ̬ªªª¬ª¬ª¬ªÊ̬ªªªŒˆˆˆÌÀÌÎÌÌÎ̬̪ªÀ쬪ÀÀÀÌîÀìîÌÌîî > =""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""ÂÌ"ÂÌ"Ì",ÌÌ,ÌÌ""""ÂÌ,ÌÂ,ÂÌ"ÂÌ,ÌÌ,"""ÂÌ,ÂÌ"Ì",ÌÌ,ÌÌ""""Â,"Â""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""Ì",Ì",Ì",Ì""Ì",""""Ì"ÌÌ,Ì",Ì""Ì"""""Ì,"Ì",Ì",Ì""Ì","""Â,"","""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""Ì""Ì",Ì",ÌÌ"Ì",""""Ì"ÌÌ,Ì",ÌÂ,ÌÌ""""ÂÌ"Ì",Ì",ÌÌ"Ì",""""""","""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""Ì",Ì",ÂÌ"Ì""ÌÌ"""""Ì",,,ÌÌ,Ì",Ì""""""Ì,ÌÌ,ÂÌ"Ì""Ì","""Â,"","""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""ÂÌ"ÂÌ"","ÌÌ,Ì","""ÂÌ,,",Ì",ÂÌ,ÌÌ,"""ÌÌ"Ì",","ÌÌ,ÌÌ""""Â,"Â""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""àîîàîààîîàààààîààààààààîÐ Ð ÐÝÐÐÐ ÐÐÝÐÝÐ ÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐ ÐÝÐ ÐÐÐ ÐÝÐÐ Ð ÐÐÐÐ ÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐÐÝ ÐÐÐÝÐ ÐÝÐ Ð Ð ÐÐÐ Ð ÝÐÐÐÐ Ð Ý Ý Ý ÝÐ Ð ÐÐ Ð ÝÐ ÐÐÐÐÐÐÐÐÐ ÝÐ ÝÝÝ ÐÐÝÐÐ ÐÐÝÐÐ ÐÝ ÐÝÐ ÐÐ Ð ÐÝ ÝÝÝÝ ÝÐÐÐÐÝ ÐÐÐÐÐÐ ÐÐÐÐÐÐÐÐÐÐ Ý Ð Ð ÐÝÐÐÝ ÐÝÝÐÝÐÐ Ð ÐÝÐÐÐÐ Ð Ð Ý ÝÝ ÝÝÝ ÝÐ Ð ÐÝÝÐÐÐÐ Ý ÝÝ Ð Ð ÝÝÐ ÐÐÐÐÐÐÐÐ ÝÐÐÐ Ð ÐÐÝÐ ÐÐ ÐÝÐÐÐ ÐÐ Ð ÝÝÐÐÝ ÐÐÐÐÐÐ ÐÐÐÐÐÐÐÐÐ Ð Ý Ð ÐÝÐ ÝÐÝÐÝÐ ÐÐÐÐ ÐÝÝ ÐÐ ÝÝ Ý À ÀÌ À ÀÀÌÀÌÀ ÀÌÌÀÀÀÀÀÀÀÀÀÀÀ Ì ÀÀÀÌÀ ÀÌÀÀ À ÀÀ À ÀÀ ÀÀÀÀÀ À ÀÀÌÀÌÀÀÀÌÀÀÀ ÀÀÌÀÀÌÀÀÀÀ ÀÀÀÌÀ ÀÀÀ ÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀ ÀÀÀ ÀÌÌÀÀÀÀÀÀÀÀ ÀÌÀÌÀÀÀ ÀÀ ÀÌ ÀÌÀ ÌÀÌÀÀÀÀÀÀÀÀÀÀ ÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀ ÀÀÀÀÀÀ ÌÀÀÌÀÀÀÀÀÌÀÀÀ ÀÀÀÀ ÀÀÀ ÀÀÌÀÀÌÀ ÀÌÌÀÌÀÀ À À ÀÀ À ÌÀÀÀÀÀÀÌÀÀÀÀÌÀÀ ÀÀÌÀÀÀÀÀÀÀÀÀÀ ÀÀÀÀÌ ÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀÀ ÀÌÀÀÌ ÀÌÌÀ ÀÀ ÀÀÀÀ À À ÀÌ ÀÌÀÀ ÀÀÀ À ÌÀ À ÀÀÀÀ ÀÌÀÀ ÀÀÌÀÀÀÀÌÀÀÀÀ ÀÀÀÀ ÀÀÀÀÀÀÀ ÀÀÀÀÀÀÀ ÀÀÀÀÀÀÌÌÀÌÀÀÌÀÌÀ À ÀÀÌÀÀÀÀ ÀÀÀ ÀÌ ÀÌÀÀ À À ÌÌ À ÌÀÀÀÀ ÀÌÀÀÀÀÌÀ ÀÀ ÀÀÀÀÀÀÀ ÀÀÀÀ ÀÀÀÀÀÀ ÀÀÀÀ ÀÀÀ ÀÀÀÀÀÀ ÀÌÀ ÌÀÌÀÀÀÌÀ ÀÀÌÀÀÀÀ ÀÀÀ ÌÌÀÌÀÀ À ÌÀ À ÌÌ ÀÌÀ ÀÀÀÀÀÀÌÀÀÀÀÀÀÌÀÀÀÀÀÀ ÌÀÀÀÀÀÀÀÀ ÀÀÀ ÀÀÀÀÀÀÀ ÀÀÀÀÀÀÀÀÀÀÀÀÀÀÌÀÌÀ ÌÌÀÌÀÀÌÀ ÀÀ ÀÀÀÀ ÀÀÀ ÀÌÀ ÌÀÀÌÀÀÀÀÀ ÀÀÀÀ À ÀÀÀÀ ÀÀÀÀ ÀÀÀÀÀ ÀÌ ÌÀÀÀ À UPUPUPPPPPPPPPPPPPPPUPUPPPPUPPPPPUUPPPPPPPPPPPPPUPUUUPPUPPPUPPPUPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPUPPPPPPPPPPPPPUPUPPPUPPPPUUPUUPPPUPPPPUUUPPPPPPUUPPPPPPUUPUUPPPPPUPUUPPPUPPPUUPUPPPPPUPPPPUPUPPPPPPUPPUPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPUPUUPUPUPPPPPPUUPUUPUPPPUUUø!-- title: RAM Probe (lua) -- author: tenfour -- cover: none -- script: lua -- input: keyboard -- -- The probe begins with 64 KiB allocations. Whenever one fails, it halves -- the chunk size and continues, eventually testing down to 1 KiB. local START_CHUNK_BYTES = 64 * 1024 local MIN_CHUNK_BYTES = 1 * 1024 local DEFAULT_ATTEMPTS_PER_FRAME = 4 local MAX_ATTEMPTS_PER_FRAME = 32 -- Blocks are kept in modest-sized page tables. This avoids one enormous -- Lua array whose occasional resize would create a large transient spike. local SLOTS_PER_PAGE = 128 -- Kept live during the test, then released after the final failure so there -- is enough memory left to draw the result and save the checkpoint. local EMERGENCY_BYTES = 128 * 1024 -- pmem layout. Values are stored as integer KiB so they fit comfortably in -- TIC-80's unsigned 32-bit persistent-memory slots. local PM_MAGIC = 0 local PM_STATE = 1 local PM_PAYLOAD_KIB = 2 local PM_HEAP_KIB = 3 local PM_BASELINE_KIB = 4 local PM_CHUNK_BYTES = 5 local PM_BLOCK_COUNT = 6 local MAGIC = 0x52414d31 -- "RAM1" local STATE_IDLE = 0 local STATE_RUNNING = 1 local STATE_CAUGHT_OOM = 2 local STATE_PAUSED = 3 local KEY_C = 3 local KEY_R = 18 local KEY_SPACE = 48 local KEY_UP = 58 local KEY_DOWN = 59 local fills = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", } local pages = {} local emergency = nil local blockCount = 0 local payloadBytes = 0 local chunkBytes = START_CHUNK_BYTES local attemptsPerFrame = DEFAULT_ATTEMPTS_PER_FRAME local started = false local running = false local failed = false local lastError = nil local baselineKiB = 0 local currentHeapKiB = 0 local failureHeapKiB = 0 local recoveryPending = false local previousValid = false local previousState = STATE_IDLE local previousPayloadKiB = 0 local previousHeapKiB = 0 local previousBaselineKiB = 0 local previousChunkBytes = 0 local previousBlockCount = 0 local function roundedKiB(value) return math.floor(value + 0.5) end local function checkpoint(state, heapKiB) pmem(PM_MAGIC, MAGIC) pmem(PM_STATE, state) pmem(PM_PAYLOAD_KIB, math.floor(payloadBytes / 1024)) pmem(PM_HEAP_KIB, roundedKiB(heapKiB)) pmem(PM_BASELINE_KIB, roundedKiB(baselineKiB)) pmem(PM_CHUNK_BYTES, chunkBytes) pmem(PM_BLOCK_COUNT, blockCount) end local function clearCheckpoint() for i = PM_MAGIC, PM_BLOCK_COUNT do pmem(i, 0) end previousValid = false end local function loadPreviousCheckpoint() if pmem(PM_MAGIC) == MAGIC then previousValid = true previousState = pmem(PM_STATE) previousPayloadKiB = pmem(PM_PAYLOAD_KIB) previousHeapKiB = pmem(PM_HEAP_KIB) previousBaselineKiB = pmem(PM_BASELINE_KIB) previousChunkBytes = pmem(PM_CHUNK_BYTES) previousBlockCount = pmem(PM_BLOCK_COUNT) end end local function stateName(state) if state == STATE_RUNNING then return "RUNNING / ABORTED" end if state == STATE_CAUGHT_OOM then return "CAUGHT OOM" end if state == STATE_PAUSED then return "PAUSED" end return "IDLE" end local function oneDecimal(value) return math.floor(value * 10 + 0.5) / 10 end local function mibFromKiB(kib) return oneDecimal(kib / 1024) end local function beginRun() -- Make any old probe data collectible before starting a fresh run. pages = nil emergency = nil collectgarbage("collect") pages = {} emergency = string.rep("!", EMERGENCY_BYTES) blockCount = 0 payloadBytes = 0 chunkBytes = START_CHUNK_BYTES failed = false lastError = nil failureHeapKiB = 0 recoveryPending = false collectgarbage("collect") baselineKiB = collectgarbage("count") currentHeapKiB = baselineKiB started = true running = true checkpoint(STATE_RUNNING, currentHeapKiB) end local function releaseProbeMemory() running = false started = false failed = false lastError = nil failureHeapKiB = 0 recoveryPending = false pages = nil emergency = nil collectgarbage("collect") pages = {} blockCount = 0 payloadBytes = 0 chunkBytes = START_CHUNK_BYTES baselineKiB = collectgarbage("count") currentHeapKiB = baselineKiB end -- Deliberately has no return value. If either the string allocation or the -- table growth fails, pcall() around this function reports the memory error. local function allocateOneBlock() local zeroBased = blockCount local pageIndex = math.floor(zeroBased / SLOTS_PER_PAGE) + 1 local slotIndex = (zeroBased % SLOTS_PER_PAGE) + 1 local page = pages[pageIndex] if page == nil then page = {} pages[pageIndex] = page end local fill = fills[(zeroBased % #fills) + 1] page[slotIndex] = string.rep(fill, chunkBytes) end local function runAllocations() for _ = 1, attemptsPerFrame do local ok, err = pcall(allocateOneBlock) if ok then blockCount = blockCount + 1 payloadBytes = payloadBytes + chunkBytes else lastError = err if chunkBytes > MIN_CHUNK_BYTES then chunkBytes = math.max( MIN_CHUNK_BYTES, math.floor(chunkBytes / 2) ) else -- This is the stable live heap at which the next -- 1 KiB allocation failed. failureHeapKiB = collectgarbage("count") running = false failed = true -- Save the result and return from this frame before doing -- a full GC or drawing. TIC-80 can then flush pmem normally. checkpoint(STATE_CAUGHT_OOM, failureHeapKiB) emergency = nil recoveryPending = true break end end end if running then currentHeapKiB = collectgarbage("count") checkpoint(STATE_RUNNING, currentHeapKiB) end end function BOOT() collectgarbage("collect") baselineKiB = collectgarbage("count") currentHeapKiB = baselineKiB loadPreviousCheckpoint() end local function draw() cls(0) local y = 3 local function line(text, color) print(text, 3, y, color or 12, false, 1, true) y = y + 7 end line("TIC-80 LUA HEAP PROBE", 14) if previousValid and not started then line( "PREV: " .. stateName(previousState), 13 ) line( "payload " .. previousPayloadKiB .. " KiB; heap " .. previousHeapKiB .. " KiB", 13 ) line( "base " .. previousBaselineKiB .. " KiB; blocks " .. previousBlockCount, 13 ) end local state = "READY" if running then state = "RUNNING" end if started and not running and not failed then state = "PAUSED" end if failed then state = "CAUGHT OOM" end local reportHeapKiB = currentHeapKiB if failed then reportHeapKiB = failureHeapKiB end line( "state: " .. state, running and 11 or (failed and 6 or 12) ) line( "chunk: " .. (chunkBytes / 1024) .. " KiB x " .. attemptsPerFrame .. "/frame" ) line( "payload: " .. mibFromKiB(payloadBytes / 1024) .. " MiB" ) line( "lua heap: " .. mibFromKiB(reportHeapKiB) .. " MiB" ) line( "baseline: " .. mibFromKiB(baselineKiB) .. " MiB" ) line( "heap delta: " .. mibFromKiB(reportHeapKiB - baselineKiB) .. " MiB" ) line("blocks: " .. blockCount) if failed then line("NEXT 1 KiB ALLOCATION FAILED", 6) line( "reserve released; current " .. mibFromKiB(currentHeapKiB) .. " MiB", 5 ) elseif lastError ~= nil then line("larger chunk failed; refining...", 5) end print( "SPACE start/pause UP/DOWN speed", 3, 119, 5, false, 1, true ) print( "R release RAM C clear saved", 3, 127, 5, false, 1, true ) end function TIC() -- This runs on the frame after the final caught allocation -- failure. The checkpoint has already returned through TIC once. if recoveryPending then collectgarbage("collect") currentHeapKiB = collectgarbage("count") recoveryPending = false end if keyp(KEY_C) then clearCheckpoint() end if keyp(KEY_R) then releaseProbeMemory() end if keyp(KEY_UP) then attemptsPerFrame = math.min( MAX_ATTEMPTS_PER_FRAME, attemptsPerFrame * 2 ) end if keyp(KEY_DOWN) then attemptsPerFrame = math.max( 1, math.floor(attemptsPerFrame / 2) ) end if keyp(KEY_SPACE) then if not started then beginRun() elseif not failed then running = not running checkpoint( running and STATE_RUNNING or STATE_PAUSED, currentHeapKiB ) end end if running then runAllocations() end -- Avoid allocating display strings in the exact-OOM frame. -- Returning lets the host save pmem first. if recoveryPending then return end draw() end