メモリ

OS7.1.0以降

LuaプラグインはobnizOSと同じCPU、同じメモリ(ヒープ)で動作します。プラグインがメモリを使いすぎると、プラグインだけでなくOS自体も動作できなくなります。たとえば、Wi-FiやクラウドへのTLS接続が確立できなくなります。

このページでは、OSがプラグインのメモリをどのように管理しているかと、プラグインのメモリ使用量を抑える方法を説明します。


OSによるメモリの管理

Luaが使えるメモリはOSと共有です。その大きさは製品やOSの使い方(Wi-Fi、LTE、BLEなど)によって変わり、最大で約200KBです。

メモリの確保で問題になるのは、空きメモリの合計ではなく連続で確保できる最大の空きブロックです。合計の空きが多くても、このブロックより大きな確保は失敗します。

OSはon_online_loop() / on_offline_loop()の実行中、約1秒ごとにこの値を確認します。

最大の空きブロック OSの動作
48KB以上 通常の状態です。Luaのガベージコレクタ(GC)を少しずつ進めます。
48KB未満 フルGCを実行して不要なオブジェクトを解放し、もう一度確認します。
フルGC後も32KB未満 メモリ確保が失敗する前にon_low_memory()を呼びます。この状態が続く間は、約1秒ごとに繰り返し呼びます。

また、メモリ確保がすでに失敗したとき(Luaのエラーnot enough memory)にも、OSは最後の手段としてon_low_memory()を呼び、フルGCを実行します。エラー自体はobniz.plugin.onErrorでobniz.jsにも通知されます。


on_low_memory(largestFreeBlock)

プラグインにこのグローバル関数を定義すると、メモリ不足の通知を受け取れます。

引数 型 説明
largestFreeBlock number 連続で確保できる最大の空きブロック(バイト)です。次のメモリ確保に使える実際の余裕を表します。

ハンドラでは、プラグインが手放せるものを解放してください。大きなテーブルやバッファ、キャッシュにnilを代入します。それらのオブジェクトは、その後にOSが実行するGCで解放されます。

local cache = {}
local logs = {}

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

  -- 手放せるものを解放する
  cache = {}
  logs = {}
end

注意点:

  • ハンドラの定義は任意です。定義していない場合、OSはGCだけを実行します。
  • 再帰を防ぐため、ハンドラの実行中に再びハンドラが呼ばれることはありません。
  • ハンドラの処理は小さく保ってください。メモリが足りない状態で呼ばれるため、ハンドラの中で新しいテーブルや長い文字列を作らないでください。
  • ハンドラの中でエラーが起きた場合、エラーはログに出力され、obniz.plugin.onErrorに通知されます。

os.getHeap()

メモリの状態はos.getHeap()で確認できます。3つの数値(バイト)を返します。

local free, minFree, largest = os.getHeap()
-- free:    現在の空きヒープ
-- minFree: 起動以降の最小空きヒープ
-- largest: 連続で確保できる最大の空きブロック

collectgarbage("count")は、Lua自体が使っているメモリをKB単位で返します。これらの値を定期的に記録すると、メモリリークを見つけやすくなります。

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

メモリ使用量を抑えるには

on_online_loop()とon_offline_loop()は約1msごと、つまり1秒間に約1,000回呼ばれます。その中で確保したものはこの頻度でゴミになり、GCが追いつかないことがあります。

  • ループの中で関数(クロージャ)を作らないでください。 pcall(function() ... end)のように書いたり、呼び出しのたびに無名関数を渡したりすると、毎回新しいオブジェクトが作られます。関数はループの外で一度だけ定義し、pcall(myFunc, arg)のように使い回してください。
  • 毎回文字列やテーブルを組み立てないでください。 重い処理は、os.getTick()を使って一定間隔で実行するなど、必要なときだけ行ってください。
  • 際限なくデータを溜めないでください。 キューやログ、キャッシュは大きさに上限を設け、古いものから捨ててください。
  • 大きなレスポンスに注意してください。 net.httpはレスポンスの本文をすべてメモリに保持します。必要なデータだけを取得してください。
  • GCのモードを切り替えないでください。 collectgarbage("generational")は呼ばないでください。端末では非対応で、変数が壊れることがあります。GCはデフォルト(インクリメンタル)のまま使ってください。
-- 悪い例: 1秒間に約1,000回、新しいクロージャを作る
function on_online_loop()
  pcall(function()
    process(uart.recv())
  end)
end

-- 良い例: 関数は一度だけ作られる
local function step()
  process(uart.recv())
end

function on_online_loop()
  pcall(step)
end