wasm_module_cache (wasm v0.1.0)
View SourceCompiled modules, cached node-wide and keyed by content hash.
You reach this through wasm:load/1 and wasm:load_file/1. Decoding and
validating is the expensive part of the pipeline, about 20 ms for a 100 KB Rust
binary against 15 us to instantiate one, and a plugin host or a
request-per-instance service compiles a small stable set of modules and
instantiates them constantly. So the compile belongs behind a cache and the
instantiate does not.
Why persistent_term
A compiled module is immutable and read on every instantiation, which is
exactly what persistent_term is for: reads do not copy, however large the
term. ETS would copy the whole intermediate representation into the reading
process every time, and that IR is megabytes for a real module.
The cost is on the other side: every put and erase triggers a global scan.
That is fine here precisely because loading is rare and instantiating is not,
but it does mean you should not call load/1 in a loop, and it makes anyone who
can drive repeated load and unload cycles a denial of service. Hence the rate
limit and the resident cap below, enforced here rather than left to you to
remember.
Identity is the content hash
Load the same bytes from two places and you get the same handle and share one
compiled artefact, with no coordination. It also means either of you can call
unload/1 safely: the module stays resident until the last holder drops it.
Summary
Types
An opaque handle to a cached module.
Functions
Fetch a cached module. It reads without copying, so you can call it on every instantiation.
Decode, validate and cache a module, returning a handle.
Drop your claim. The module stays resident until the last holder goes.
Types
-type annotated() :: {Height :: non_neg_integer(), instr()} | {Height :: non_neg_integer(), Base :: non_neg_integer(), instr()}.
-type externtype() :: {func, typeidx()} | {table, #tabletype{limits :: #limits{min :: non_neg_integer(), max :: undefined | non_neg_integer(), shared :: boolean(), index_type :: i32 | i64}, elemtype :: reftype(), init :: undefined | [instr()]}} | {mem, #memtype{limits :: #limits{min :: non_neg_integer(), max :: undefined | non_neg_integer(), shared :: boolean(), index_type :: i32 | i64}}} | {global, #globaltype{valtype :: valtype(), mut :: mut()}} | {tag, #tagtype{type :: typeidx()}}.
-type funcidx() :: non_neg_integer().
-nominal handle() :: {wasm_module, binary()}.
An opaque handle to a cached module.
-type heaptype() :: func | extern | exn | any | eq | i31 | struct | array | nofunc | noextern | noexn | none | {type, typeidx()}.
-type memidx() :: non_neg_integer().
-type mut() :: const | var.
-type numtype() :: i32 | i64 | f32 | f64.
-type reftype() :: {ref, null | nonull, heaptype()}.
-type tableidx() :: non_neg_integer().
-type typeidx() :: non_neg_integer().
-type vectype() :: v128.
Functions
-spec get(handle()) -> {ok, #module{identity :: undefined | {sha256, binary()} | reference(), types :: [#subtype{final :: boolean(), supers :: [typeidx()], body :: #functype{params :: [valtype()], results :: [valtype()]} | #structtype{fields :: [#fieldtype{type :: valtype() | i8 | i16, mut :: mut()}]} | #arraytype{field :: #fieldtype{type :: valtype() | i8 | i16, mut :: mut()}}}], rec_groups :: [{non_neg_integer(), non_neg_integer()}], imports :: [#import{module :: binary(), name :: binary(), desc :: externtype()}], funcs :: [#func{type :: typeidx(), locals :: [valtype()], body :: [instr()] | {validated, [annotated()]}}], tables :: [#tabletype{limits :: #limits{min :: non_neg_integer(), max :: undefined | non_neg_integer(), shared :: boolean(), index_type :: i32 | i64}, elemtype :: reftype(), init :: undefined | [instr()]}], mems :: [#memtype{limits :: #limits{min :: non_neg_integer(), max :: undefined | non_neg_integer(), shared :: boolean(), index_type :: i32 | i64}}], tags :: [#tagtype{type :: typeidx()}], globals :: [#global{type :: #globaltype{valtype :: valtype(), mut :: mut()}, init :: [instr()]}], exports :: [#export{name :: binary(), desc :: {func | table | mem | global | tag, non_neg_integer()}}], start :: undefined | funcidx(), elems :: [#elem{type :: reftype(), init :: [[instr()]], mode :: passive | declarative | {active, tableidx(), [instr()]}}], datas :: [#data{init :: binary(), mode :: passive | {active, memidx(), [instr()]}}], data_count :: undefined | non_neg_integer(), customs :: [{binary(), binary()}]}} | {error, not_loaded}.
Fetch a cached module. It reads without copying, so you can call it on every instantiation.
Decode, validate and cache a module, returning a handle.
Load the same bytes twice and the second call is cheap: it finds the compiled artefact already resident and only bumps its holder count.
-spec resident() -> non_neg_integer().
-spec stats() -> map().
-spec unload(handle()) -> ok.
Drop your claim. The module stays resident until the last holder goes.
Claims are per process: this drops one held by the calling process, and a process that dies without calling it has its claims dropped for it.