wasm_module_cache (wasm v0.1.0)

View Source

Compiled 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

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

annotated()

-type annotated() ::
          {Height :: non_neg_integer(), instr()} |
          {Height :: non_neg_integer(), Base :: non_neg_integer(), instr()}.

externtype()

-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()}}.

funcidx()

-type funcidx() :: non_neg_integer().

handle()

-nominal handle() :: {wasm_module, binary()}.

An opaque handle to a cached module.

heaptype()

-type heaptype() ::
          func | extern | exn | any | eq | i31 | struct | array | nofunc | noextern | noexn | none |
          {type, typeidx()}.

instr()

-type instr() :: atom() | tuple().

memidx()

-type memidx() :: non_neg_integer().

mut()

-type mut() :: const | var.

numtype()

-type numtype() :: i32 | i64 | f32 | f64.

reftype()

-type reftype() :: {ref, null | nonull, heaptype()}.

tableidx()

-type tableidx() :: non_neg_integer().

typeidx()

-type typeidx() :: non_neg_integer().

valtype()

-type valtype() :: numtype() | vectype() | reftype().

vectype()

-type vectype() :: v128.

Functions

get/1

-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.

handle_call/3

handle_cast(Msg, State)

handle_info/2

init/1

load(Binary)

-spec load(binary()) -> {ok, handle()} | {error, term()}.

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.

load(Binary, Opts)

-spec load(binary(), map()) -> {ok, handle()} | {error, term()}.

resident()

-spec resident() -> non_neg_integer().

start_link()

stats()

-spec stats() -> map().

unload/1

-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.