wasm_engine (wasm v0.1.0)
View SourceNode-wide resource accounting.
This is where you cap how much linear memory the whole node may hold, and where you go to see how much is in use:
wasm_engine:set_page_limit(16384), % 1 GiB
#{pages_in_use := N} = wasm_engine:stats().Set the cap. Linear memory is backed by atomics arrays, which live outside the
process heap. That is what makes them fast (see the benchmark table in the
design notes), but it also means max_heap_size cannot see them: a module can
exhaust node memory without its owning process's heap ever moving. So page
accounting has to be explicit, and it has to be node-wide rather than
per-instance, because a thousand small instances are as much of a threat as one
large one.
Reading the counter is a lock-free atomics get, which is what keeps it off the
cost of an access. Moving it is not: reservation and release happen inside a
wasm_keeper transaction, together with the registry row that says whose pages
they are. Reserving here and recording the holder afterwards is exactly how the
two came apart, and a counter that disagrees with the registry is a counter that
eventually refuses every allocation on the node.
Summary
Functions
The shared store, under its general name.
Make sure the shared store exists.
Create the waiter table if it is not there.
Make sure the waiter table exists, owned by something that outlives waiters.
Intern a canonical recursive type group, returning its node-wide identity.
Reserve N pages against the node-wide budget.
Cap the total linear memory pages every instance on this node may hold together.
Start the engine, or adopt the one that is already there.
Read the current page budget and what is in use.
The largest number of entries a table may hold.
Functions
-spec cell_forget(reference()) -> ok.
The shared store, under its general name.
wasm_table was the first user, so the rows are called tables; a shared
mutable global is one term in the same store, with the same lifetime rules.
-spec ensure_store() -> ok.
Make sure the shared store exists.
wasm_sup calls this so the store belongs to the supervisor. It holds every
table's contents, every shared global's value and every published chunk tuple,
and wasm_keeper's registry names rows in it: an engine restart that took the
store with it would leave the registry pointing at nothing.
-spec ensure_waiter_table() -> ok.
Create the waiter table if it is not there.
wasm_sup calls this so the table belongs to the supervisor: an engine restart
would otherwise strand every parked agent, since a wait is a receive that
only a row in this table can be found by.
-spec ensure_waiters() -> ok.
Make sure the waiter table exists, owned by something that outlives waiters.
wasm_wait calls this rather than creating the table itself. An ETS table dies
with the process that created it, and a waiter is a guest's process: when one
was killed on a worker timeout it took every other agent's registration with
it, and the next notify woke nobody.
There is no fallback to creating it locally. That fallback existed so a threaded module could run without the application, and it reintroduced exactly the defect it sits next to: an owner that is a waiter. An unsupervised engine is started on demand instead, so there is one behaviour to reason about and one that gets tested.
-spec intern_rec_group(term()) -> non_neg_integer().
Intern a canonical recursive type group, returning its node-wide identity.
Node-wide because type identity has to hold across modules: one module imports a function whose type another declared, and they must agree it is the same type.
Lock-free. A racing pair may both allocate an id, but only one insert_new
wins and the loser re-reads the winner's, so an id is never handed to two
different groups. Interning happens once per group at compile time, so the
retry costs nothing worth avoiding.
-spec page_limit() -> non_neg_integer().
-spec pages_in_use() -> non_neg_integer().
-spec release_pages(non_neg_integer()) -> ok.
-spec reserve_pages(non_neg_integer()) -> ok | {error, limit}.
Reserve N pages against the node-wide budget.
Call this from wasm_keeper and nowhere else: a reservation that is not
recorded against a holder in the same step belongs to nobody, and nothing will
ever give it back. You get {error, limit} rather than an exception, because
memory.grow has to turn a refusal into the value -1 that the specification
requires, not into a trap.
-spec set_page_limit(non_neg_integer()) -> ok.
Cap the total linear memory pages every instance on this node may hold together.
Set this once at startup if you run modules you do not control. A page is
64 KiB, so set_page_limit(16384) is 1 GiB. Reserving past the cap makes
memory.grow return -1 to the module, as the specification requires, rather
than trapping.
Start the engine, or adopt the one that is already there.
The waiter table needs an owner that outlives any waiter, and a threaded module
can be run before the application is started. So the engine, like
wasm_keeper, may already exist by the time the supervisor gets here; adopting
it keeps the table it owns instead of taking every parked agent's registration
down with it.
Read the current page budget and what is in use.
-spec table_forget(reference()) -> ok.
-spec table_grow_limit() -> non_neg_integer().
The largest number of entries a table may hold.
Set it with the table_grow_limit application environment key. Unlike the page
budget it bounds each table rather than their node-wide total, because what it
is there for is stopping one table.grow from doing unbounded work.
-spec unregister_limits(pid()) -> ok.