Memory

Available for OS7.1.0 and later

A Lua plugin runs on the same CPU and in the same memory (heap) as obnizOS. When the plugin uses too much memory, not only the plugin but also the OS itself can fail: for example, Wi-Fi or a TLS connection to the cloud can no longer be established.

This page explains how the OS manages memory for the plugin, and how to keep the plugin's memory use under control.


How the OS manages memory

The memory available to Lua is shared with the OS. Its size depends on the product and on how the OS is used (Wi-Fi, LTE, BLE, and so on), and is up to about 200 KB.

What matters for an allocation is not the total free memory but the largest contiguous free block. Even when the total free memory is large, an allocation larger than this block fails.

The OS checks this value about once per second while on_online_loop() / on_offline_loop() are running:

Largest free block What the OS does
48 KB or more Normal. The Lua garbage collector (GC) runs step by step.
Less than 48 KB Runs a full GC to free unused objects, then checks again.
Still less than 32 KB after the full GC Calls on_low_memory() before an allocation fails. While the condition continues, it is called again about once per second.

In addition, when an allocation has already failed (the Lua error not enough memory), the OS calls on_low_memory() as a last resort and runs a full GC. The error itself is also reported to obniz.js through obniz.plugin.onError.


on_low_memory(largestFreeBlock)

Define this global function in your plugin to receive the low-memory notification.

Argument Type Description
largestFreeBlock number The largest contiguous free block, in bytes. This is the real headroom for the next allocation.

In the handler, release what the plugin can spare: set large tables, buffers, and caches to nil. The objects are freed by the GC that the OS runs afterwards.

local cache = {}
local logs = {}

function on_low_memory(largestFreeBlock)
  os.log("low memory: " .. tostring(largestFreeBlock))

  -- Release what you can spare
  cache = {}
  logs = {}
end

Notes:

  • The handler is optional. If it is not defined, the OS only runs the GC.
  • To avoid recursion, the handler is not called again while it is already running.
  • Keep the handler small. Avoid creating new tables or long strings inside it, because it runs when memory is already short.
  • If the handler raises an error, the error is logged and reported to obniz.plugin.onError.

os.getHeap()

Use os.getHeap() to check the memory status yourself. It returns three numbers, in bytes.

local free, minFree, largest = os.getHeap()
-- free:    current free heap
-- minFree: minimum free heap since boot
-- largest: largest contiguous free block

collectgarbage("count") returns the memory used by Lua itself, in KB. Recording these values periodically helps you find a leak.

local last = 0

function on_online_loop()
  local now = os.getTick()
  if now - last < 60 * 1000 then
    return
  end
  last = now

  local free, minFree, largest = os.getHeap()
  os.log(string.format("lua=%dKB free=%d min=%d largest=%d",
    math.floor(collectgarbage("count")), free, minFree, largest))
end

Keeping memory use low

on_online_loop() and on_offline_loop() are called about every 1 ms, which is about 1,000 times per second. Anything allocated in them becomes garbage at that rate, and the GC may not keep up.

  • Do not create functions (closures) in the loop. Writing pcall(function() ... end) or passing an anonymous function in each call creates a new object every time. Define the function once outside the loop and reuse it, for example pcall(myFunc, arg).
  • Avoid building strings and tables in every iteration. Run heavy work only when needed, for example at a fixed interval using os.getTick().
  • Do not keep unbounded data. Limit the size of queues, logs, and caches, and drop the oldest entries.
  • Watch large responses. net.http keeps the whole response body in memory. Request only what you need.
  • Do not switch the GC mode. Do not call collectgarbage("generational"). It is not supported on the device and can break variables. Leave the GC in its default (incremental) mode.
-- Bad: creates a new closure about 1,000 times per second
function on_online_loop()
  pcall(function()
    process(uart.recv())
  end)
end

-- Good: the function is created only once
local function step()
  process(uart.recv())
end

function on_online_loop()
  pcall(step)
end