wasm_snapshot (wasm v0.3.0)
View SourceInitialized runtime snapshots: an immutable copy of an already-started guest.
Captured after a runtime's init() has returned and while no call is running,
and restored into a fresh instance at that point, so interpreter startup is
skipped and one-instance-per-request isolation is kept exactly as it was.
Call it an initialized runtime snapshot or an instance snapshot, and not a heap
snapshot: wasm_heap here already means the garbage-collected object store,
which is a narrower thing and is in fact one of the states a snapshot refuses
to carry.
What it holds
Every linear memory's bytes, size and limits; mutable globals; table contents; and the dropped data and element segment sets. Nothing else, and the omissions are the design:
Ownership metadata is exactly what must not be captured. Keeper resource
ids, holder tokens, owner pids and live handles are identities of this
instance in this node at this moment, and an image carrying them would
restore into references to something already destroyed. Restore mints fresh
ones, reserving through wasm_keeper as instantiation does. What enters the
image is only what can be reconstructed from it.
Host and operating-system resources are reconstructed per request, never serialised: open files and directories, sockets, clocks, random providers, Erlang closures and processes, and whatever state an arbitrary host import maintains. The caller builds those fresh for each restore.
What it refuses
Loudly, and never half-applied:
| why | |
|---|---|
| an instance with no retained module handle | provenance cannot be proved |
| an imported memory, table or global | the aliasing an image cannot represent |
| a shared memory | the same, plus another thread may be writing it |
a non-empty wasm_heap | references are ids into a store, not values |
an external funcref or any externref | it names an instance that is not this one |
The first is the one that is easy to get wrong. An identity is a name and
wasm:compile/2 lets a caller supply one, so loading module A under hash H
and compiling a different B with identity => {sha256, H} gives B a claim on
H that succeeds. A claim proves the cache holds something under that name,
never that this instance came from it, so #inst.module_handle is retained at
instantiation and capture requires it.
Restore does not run the start function
Ordinary instantiation always does, and a start function is arbitrary guest code with arbitrary host effects: running it during a restore would repeat, against fresh imports, work the captured image already contains. This is the single most important sentence here.
Summary
Types
What a hook may keep: no pid, port, reference or fun.
An immutable image. Copyable between processes; see wasm:acquire/1.
Functions
Capture, given the module the instance was built from.
An image from what a file held, bound to a module the caller resolved.
Size, module and version, so a size has a way to be read.
The address space an image covers, as against the bytes it retains.
The process holding this image, or undefined before one is attached.
Build a fresh instance at the captured point.
An image's contents, in the plain shape wasm_snapshot_file encodes.
Attach the process that holds this image's claim and its charge.
Types
-type annotated() :: {Height :: non_neg_integer(), instr()} | {Height :: non_neg_integer(), Base :: non_neg_integer(), instr()}.
-type captured_mem() :: #{pages := non_neg_integer(), runs := [{non_neg_integer(), binary()}]}.
-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().
-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 portable() :: binary() | number() | atom() | [portable()] | tuple() | #{portable() => portable()}.
What a hook may keep: no pid, port, reference or fun.
Restricted by construction and checked at capture, because a hook free to return any term could quietly contradict the promise that host resources are never captured.
-type reftype() :: {ref, null | nonull, heaptype()}.
-nominal snapshot() :: #snapshot{id :: reference(), handle :: wasm_module_cache:handle(), version :: binary(), key :: term(), source :: reference(), globals :: tuple(), tables :: [[term()]], mems :: [captured_mem()], dropped_elems :: map(), dropped_datas :: map(), hooks :: #{binary() => portable()}, bytes :: non_neg_integer(), owner :: undefined | pid()}.
An immutable image. Copyable between processes; see wasm:acquire/1.
-type tableidx() :: non_neg_integer().
-type typeidx() :: non_neg_integer().
-type vectype() :: v128.
Functions
-spec bytes(snapshot()) -> non_neg_integer().
-spec capture(#inst{id :: reference(), ckpt :: reference(), entry_key :: undefined | reference(), types :: tuple(), funcs :: tuple(), exports :: #{binary() => {func | table | mem | global, non_neg_integer()}}, elems :: tuple(), datas :: tuple(), globaltypes :: tuple(), tags :: tuple(), canon :: tuple(), fields :: tuple(), kinds :: tuple(), supers :: tuple(), heap :: undefined | wasm_heap:heap(), identity :: undefined | {sha256, binary()} | reference(), module_handle :: undefined | wasm_module_cache:handle(), leases :: undefined | atomics:atomics_ref(), store :: term(), version :: term(), limits :: map(), ctx :: term()}, wasm_module_cache:handle(), map()) -> {ok, snapshot()} | {error, wasm_error:error()}.
Capture, given the module the instance was built from.
The caller resolves the handle rather than this module doing it, which keeps
wasm_snapshot out of the wasm and wasm_module_cache cycle:
wasm_architecture_SUITE documents exactly three, and a fourth member is a
change to the architecture rather than a detail.
-spec from_parts(wasm_snapshot_file:parts(), wasm_module_cache:handle()) -> {ok, snapshot()} | {error, wasm_error:error()}.
An image from what a file held, bound to a module the caller resolved.
The caller supplies the handle rather than the file naming one it trusts: a
planted image would otherwise pick whichever resident module suits it, which is
the forgery restore/3 closes on the live path by taking the module from the
image. Off disk the direction inverts, so the caller decides which module
an image is for and this refuses one that does not match.
-spec info(snapshot()) -> #{bytes := non_neg_integer(), module := wasm_module_cache:handle(), version := binary()}.
Size, module and version, so a size has a way to be read.
-spec logical_bytes(snapshot()) -> non_neg_integer().
The address space an image covers, as against the bytes it retains.
bytes is what the budget charges and what an image holds; this is what a
restore will write into. They differ by about 6x on a started CPython, and a
decompression ceiling has to come from the second.
-spec module_of(snapshot()) -> wasm_module_cache:handle().
The process holding this image, or undefined before one is attached.
-spec restore(snapshot(), #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()}]}, map(), map()) -> {ok, #inst{id :: reference(), ckpt :: reference(), entry_key :: undefined | reference(), types :: tuple(), funcs :: tuple(), exports :: #{binary() => {func | table | mem | global, non_neg_integer()}}, elems :: tuple(), datas :: tuple(), globaltypes :: tuple(), tags :: tuple(), canon :: tuple(), fields :: tuple(), kinds :: tuple(), supers :: tuple(), heap :: undefined | wasm_heap:heap(), identity :: undefined | {sha256, binary()} | reference(), module_handle :: undefined | wasm_module_cache:handle(), leases :: undefined | atomics:atomics_ref(), store :: term(), version :: term(), limits :: map(), ctx :: term()}} | {error, wasm_error:error()} | {error, wasm_error:error(), #inst{id :: reference(), ckpt :: reference(), entry_key :: undefined | reference(), types :: tuple(), funcs :: tuple(), exports :: #{binary() => {func | table | mem | global, non_neg_integer()}}, elems :: tuple(), datas :: tuple(), globaltypes :: tuple(), tags :: tuple(), canon :: tuple(), fields :: tuple(), kinds :: tuple(), supers :: tuple(), heap :: undefined | wasm_heap:heap(), identity :: undefined | {sha256, binary()} | reference(), module_handle :: undefined | wasm_module_cache:handle(), leases :: undefined | atomics:atomics_ref(), store :: term(), version :: term(), limits :: map(), ctx :: term()}}.
Build a fresh instance at the captured point.
The caller does not supply the module. It comes from the handle the image
retained, which is why this takes a snapshot rather than being an option to
instantiate/3. That option reopened the forgery on the other side: capture
module A under H, then restore with an inline module B built with
identity => {sha256, H} and a matching key, and the image is laid over a
different module's layout. Taking the module from the image closes it by
construction, because there is no argument left to forge.
-spec to_parts(snapshot()) -> wasm_snapshot_file:parts().
An image's contents, in the plain shape wasm_snapshot_file encodes.
Self-references are normalised. A funcref names its defining instance by
a reference(), which means nothing outside this node and nothing after a
restart, so every one is rewritten to the atom self on the way out and back
to a fresh identity on the way in. That is the same relocation reloc/3 does
between two live instances, carried across a file.
Attach the process that holds this image's claim and its charge.