diff --git a/docs/references/_attachments/ic.did b/docs/references/_attachments/ic.did index b60b731a37..d9787f2c9c 100644 --- a/docs/references/_attachments/ic.did +++ b/docs/references/_attachments/ic.did @@ -16,6 +16,7 @@ type canister_settings = record { reserved_cycles_limit : opt nat; log_visibility : opt log_visibility; wasm_memory_limit : opt nat; + wasm_memory_threshold : opt nat; }; type definite_canister_settings = record { @@ -26,6 +27,7 @@ type definite_canister_settings = record { reserved_cycles_limit : nat; log_visibility : log_visibility; wasm_memory_limit : nat; + wasm_memory_threshold: nat; }; type change_origin = variant { diff --git a/docs/references/ic-interface-spec.md b/docs/references/ic-interface-spec.md index 790147a06c..190c174329 100644 --- a/docs/references/ic-interface-spec.md +++ b/docs/references/ic-interface-spec.md @@ -1262,6 +1262,8 @@ In order for a WebAssembly module to be usable as the code for the canister, it - If it exports a function called `canister_heartbeat`, the function must have type `() -> ()`. +- If it exports a function called `canister_on_low_wasm_memory`, the function must have type `() -> ()`. + - If it exports a function called `canister_global_timer`, the function must have type `() -> ()`. - If it exports any functions called `canister_update `, `canister_query `, or `canister_composite_query ` for some `name`, the functions must have type `() -> ()`. @@ -1314,6 +1316,8 @@ The canister provides entry points which are invoked by the IC under various cir - The canister may export functions with name `canister_composite_query ` and type `() -> ()`. +- The canister may export a function with the name `canister_on_low_wasm_memory` and type `() -> ()`. + - The canister table may contain functions of type `(env : I) -> ()` which may be used as callbacks for inter-canister calls and composite query methods. The value of `I ∈ {i32, i64}` specifying whether the imported functions have 32-bit or 64-bit pointers is derived from the bit-width of the declared memory defaulting to `I = i32` if the canister declares no memory. @@ -1390,6 +1394,17 @@ While an implementation will likely try to keep the interval between the value o ::: +#### On Low Wasm Memory {#on-low-wasm-memory} + +A canister can export a function with the name `canister_on_low_wasm_memory`, which is scheduled whenever the canister's remaining wasm memory size in bytes falls from at least a threshold `t` to strictly less than `t`. +The threshold `t` can be defined in the field `wasm_memory_threshold` in the [canister's settings](#ic-update_settings) and by default it is set to 0. + +:::note + +While the above function is scheduled immediately once the condition above is triggered, it may not necessarily be executed immediately if the canister does not have enough cycles. +If the canister gets frozen immediately after the function is scheduled for execution, the function will run once the canister's unfrozen _if_ the canister's remaining wasm memory size in bytes remains strictly less than the threshold `t`. +::: + #### Callbacks Callbacks are addressed by their table index (as a proxy for a Wasm `funcref`). @@ -1542,7 +1557,7 @@ The comment after each function lists from where these functions may be invoked: - `F`: from `canister_inspect_message` -- `T`: from *system task* (`canister_heartbeat` or `canister_global_timer`) +- `T`: from *system task* (`canister_heartbeat` or `canister_global_timer` or `canister_on_low_wasm_memory`) - `*` = `I G U RQ NRQ CQ Ry Rt CRy CRt C CC F T` (NB: Not `(start)`) @@ -2150,6 +2165,13 @@ The optional `settings` parameter can be used to set the following settings: Note: in a future release of this specification, the default value and whether the limit is enforced for global timers and heartbeats might change. +- `wasm_memory_threshold` (`nat`) + + Must be a number between 0 and 264-1, inclusively, and indicates the threshold on the remaining wasm memory size of the canister in bytes: + if the remaining wasm memory size of the canister is below the threshold, execution of the ["on low wasm memory" hook](#on-low-wasm-memory) is scheduled. + + Default value: 0 (i.e., the "on low wasm memory" hook is never scheduled). + The optional `sender_canister_version` parameter can contain the caller's canister version. If provided, its value must be equal to `ic0.canister_version`. Until code is installed, the canister is `Empty` and behaves like a canister that has no public methods. @@ -2271,6 +2293,8 @@ Indicates various information about the canister. It contains: - The WASM heap memory limit of the canister in bytes (the value of `0` means that there is no explicit limit). + - The "low wasm memory" threshold, which is used to determine when the [canister_on_low_wasm_memory](#on-low-wasm-memory) function is executed. + - A SHA256 hash of the module installed on the canister. This is `null` if the canister is empty. - The actual memory usage of the canister. @@ -3265,6 +3289,7 @@ The [WebAssembly System API](#system-api) is relatively low-level, and some of i composite_query_methods : MethodName ↦ ((Arg, CallerId, Env) -> CompositeQueryFunc) heartbeat : (Env) -> SystemTaskFunc global_timer : (Env) -> SystemTaskFunc + on_low_wasm_memory : (Env) -> SystemTaskFunc callbacks : (Callback, Response, RefundedCycles, Env, AvailableCycles) -> UpdateFunc composite_callbacks : (Callback, Response, Env) -> UpdateFunc inspect_message : (MethodName, WasmState, Arg, CallerId, Env) -> Trap | Return { @@ -3332,6 +3357,7 @@ EntryPoint | Callback Callback Response RefundedCycles | Heartbeat | GlobalTimer + | OnLowWasmMemory Message = CallMessage { origin : CallOrigin; @@ -3489,6 +3515,10 @@ CanisterLog = { timestamp_nanos : Nat; content : Blob; } +OnLowWasmMemoryHookStatus + = ConditionNotSatisfied + | Ready + | Executed QueryStats = { timestamp : Timestamp; num_instructions : Nat; @@ -3525,6 +3555,8 @@ S = { reserved_balances: CanisterId ↦ Nat; reserved_balance_limits: CanisterId ↦ Nat; wasm_memory_limit: CanisterId ↦ Nat; + wasm_memory_threshold: CanisterId ↦ Nat; + on_low_wasm_memory_hook_status: CanisterId ↦ OnLowWasmMemoryHookStatus; certified_data: CanisterId ↦ Blob; canister_history: CanisterId ↦ CanisterHistory; canister_log_visibility: CanisterId ↦ CanisterLogVisibility; @@ -3630,6 +3662,8 @@ The initial state of the IC is reserved_balances = (); reserved_balance_limits = (); wasm_memory_limit = (); + wasm_memory_threshold = (); + on_low_wasm_memory_hook_status = (); certified_data = (); canister_history = (); canister_log_visibility = (); @@ -4048,10 +4082,85 @@ S with ``` +*Call context creation: On low wasm memory* + +If `S.on_low_wasm_memory_hook_status[C]` is `Ready` for a canister `C`, the IC will create the corresponding call context and set `S.on_low_wasm_memory_hook_status[C]` to `Executed`. + +Conditions + +```html + +S.canisters[C] ≠ EmptyCanister +S.canister_status[C] = Running +S.on_low_wasm_memory_hook_status[C] = Ready +liquid_balance(S, C) ≥ MAX_CYCLES_PER_MESSAGE +Ctxt_id ∉ dom(S.call_contexts) + +``` + +State after + +```html + +S with + messages = + FuncMessage { + call_context = Ctxt_id; + receiver = C; + entry_point = OnLowWasmMemory; + queue = Queue { from = System; to = C }; + } + · S.messages + call_contexts[Ctxt_id] = { + canister = C; + origin = FromSystemTask; + needs_to_respond = false; + deleted = false; + available_cycles = 0; + } + on_low_wasm_memory_hook_status[C] = Executed + balances[C] = S.balances[C] - MAX_CYCLES_PER_MESSAGE + +``` + The IC can execute any message that is at the head of its queue, i.e. there is no older message with the same abstract `queue` field. The actual message execution, if successful, may enqueue further messages and --- if the function returns a response --- record this response. The new call and response messages are enqueued at the end. Note that new messages are executed only if the canister is Running and is not frozen. +#### Scheduling on low wasm memory hook {#rule-on-low-wasm-memory} + +This transition is executed immediately after [Message execution](#rule-message-execution) and IC Management Canister execution (update call). + +Conditions + +```html +Total_memory_usage = memory_usage_wasm_state(S.canisters[C].wasm_state) + + memory_usage_raw_module(S.canisters[C].raw_module) + + memory_usage_canister_history(S.canister_history[C]) + + memory_usage_chunk_store(S.chunk_store[C]) + + memory_usage_snapshot(S.snapshots[C]) + +if S.memory_allocation[C] = 0: + Wasm_memory_capacity = S.wasm_memory_limit[C] +else: + Wasm_memory_capacity = min(S.memory_allocation[C] - (Total_memory_usage - |S.canisters[C].wasm_state.store.mem|), S.wasm_memory_limit[C]) + +if Wasm_memory_capacity < |S.canisters[C].wasm_state.store.mem| + S.wasm_memory_threshold[C]: + if S.on_low_wasm_memory_hook_status[C] = ConditionNotSatisfied: + On_low_wasm_memory_hook_status = Ready + else: + On_low_wasm_memory_hook_status = S.on_low_wasm_memory_hook_status[C] +else: + On_low_wasm_memory_hook_status = ConditionNotSatisfied +``` + +State after + +```html +S with + on_low_wasm_memory_hook_status[C] = On_low_wasm_memory_hook_status +``` + #### Message execution {#rule-message-execution} The transition models the actual execution of a message, whether it is an initial call to a public method or a response. In either case, a call context already exists (see transition "Call context creation"). @@ -4062,6 +4171,8 @@ Conditions S.messages = Older_messages · FuncMessage M · Younger_messages (M.queue = Unordered) or (∀ CallMessage M' | FuncMessage M' ∈ Older_messages. M'.queue ≠ M.queue) +(∀ FuncMessage M' ∈ Older_messages · Younger_messages. M'.receiver ≠ M.receiver or M.entry_point ≠ OnLowWasmMemory) +S.on_low_wasm_memory_hook_status[M.receiver] ≠ Ready S.canisters[M.receiver] ≠ EmptyCanister Mod = S.canisters[M.receiver].module @@ -4118,6 +4229,12 @@ or New_canister_version = S.canister_version[M.receiver] + 1 Wasm_memory_limit = 0 ) +or +( M.entry_point = OnLowWasmMemory + F = system_task_as_update(Mod.on_low_wasm_memory, Env) + New_canister_version = S.canister_version[M.receiver] + 1 + Wasm_memory_limit = 0 +) R = F(S.canisters[M.receiver].wasm_state) @@ -4157,11 +4274,12 @@ if New_reserved_balance, Min_balance ) ≥ 0 - (S.memory_allocation[M.receiver] = 0) or (memory_usage_wasm_state(res.new_state) + + Total_memory_usage = memory_usage_wasm_state(res.new_state) + memory_usage_raw_module(S.canisters[M.receiver].raw_module) + memory_usage_canister_history(S.canister_history[M.receiver]) + memory_usage_chunk_store(S.chunk_store[M.receiver]) + - memory_usage_snapshot(S.snapshots[M.receiver]) ≤ S.memory_allocation[M.receiver]) + memory_usage_snapshot(S.snapshots[M.receiver]) + (S.memory_allocation[M.receiver] = 0) or (Total_memory_usage ≤ S.memory_allocation[M.receiver]) (Wasm_memory_limit = 0) or |res.new_state.store.mem| <= Wasm_memory_limit (res.response = NoResponse) or S.call_contexts[M.call_context].needs_to_respond then @@ -4400,6 +4518,10 @@ if A.settings.wasm_memory_limit is not null: New_wasm_memory_limit = A.settings.wasm_memory_limit else: New_wasm_memory_limit = 0 +if A.settings.wasm_memory_threshold is not null: + New_wasm_memory_threshold = A.settings.wasm_memory_threshold +else: + New_wasm_memory_threshold = 0 Cycles_reserved = cycles_to_reserve(S, Canister_id, New_compute_allocation, New_memory_allocation, null, EmptyCanister.wasm_state) New_balance = M.transferred_cycles - Cycles_reserved @@ -4445,6 +4567,8 @@ S' = S with reserved_balances[Canister_id] = New_reserved_balance reserved_balance_limits[Canister_id] = New_reserved_balance_limit wasm_memory_limit[Canister_id] = New_wasm_memory_limit + wasm_memory_threshold[Canister_id] = New_wasm_memory_threshold + on_low_wasm_memory_hook_status[Canister_id] = ConditionNotSatisfied certified_data[Canister_id] = "" query_stats[Canister_id] = [] canister_history[Canister_id] = New_canister_history @@ -4496,11 +4620,17 @@ M.method_name = 'update_settings' M.arg = candid(A) M.caller ∈ S.controllers[A.canister_id] +Total_memory_usage = memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) + + memory_usage_raw_module(S.canisters[A.canister_id].raw_module) + + memory_usage_canister_history(New_canister_history) + + memory_usage_chunk_store(S.chunk_store[A.canister_id]) + + memory_usage_snapshot(S.snapshots[A.canister_id]) + if New_memory_allocation > 0: - memory_usage_wasm_state(S.canisters[A.canister_id].wasm_state) + - memory_usage_raw_module(S.canisters[A.canister_id].raw_module) + - memory_usage_canister_history(New_canister_history) + - memory_usage_snapshot(S.snapshots[A.canister_id]) ≤ New_memory_allocation + Total_memory_usage ≤ New_memory_allocation + +if New_wasm_memory_limit > 0: + |S.canisters[A.canister_id].wasm_state.store.mem| ≤ New_wasm_memory_limit if A.settings.compute_allocation is not null: New_compute_allocation = A.settings.compute_allocation @@ -4522,6 +4652,10 @@ if A.settings.wasm_memory_limit is not null: New_wasm_memory_limit = A.settings.wasm_memory_limit else: New_wasm_memory_limit = S.wasm_memory_limit[A.canister_id] +if A.settings.wasm_memory_threshold is not null: + New_wasm_memory_threshold = A.settings.wasm_memory_threshold +else: + New_wasm_memory_threshold = S.wasm_memory_threshold[A.canister_id] Cycles_reserved = cycles_to_reserve(S, A.canister_id, New_compute_allocation, New_memory_allocation, S.snapshots[A.canister_id], S.canisters[A.canister_id].wasm_state) New_balance = S.balances[A.canister_id] - Cycles_reserved @@ -4566,6 +4700,7 @@ S' = S with reserved_balances[A.canister_id] = New_reserved_balance reserved_balance_limits[A.canister_id] = New_reserved_balance_limit wasm_memory_limit[A.canister_id] = New_wasm_memory_limit + wasm_memory_threshold[A.canister_id] = New_wasm_memory_threshold canister_version[A.canister_id] = S.canister_version[A.canister_id] + 1 if A.settings.log_visibility is not null: canister_log_visibility[A.canister_id] = A.settings.log_visibility @@ -4616,6 +4751,7 @@ S with freezing_threshold = S.freezing_threshold[A.canister_id]; reserved_cycles_limit = S.reserved_balance_limit[A.canister_id]; wasm_memory_limit = S.wasm_memory_limit[A.canister_id]; + wasm_memory_threshold = S.wasm_memory_threshold[A.canister_id]; } module_hash = if S.canisters[A.canister_id] = EmptyCanister @@ -4839,12 +4975,14 @@ liquid_balance(S, A.canister_id) ≥ MAX_CYCLES_PER_MESSAGE liquid_balance(S', A.canister_id) ≥ 0 +Total_memory_usage = memory_usage_wasm_state(New_state) + + memory_usage_raw_module(A.wasm_module) + + memory_usage_canister_history(New_canister_history) + + memory_usage_chunk_store(S.chunk_store[A.canister_id]) + + memory_usage_snapshot(S.snapshots[A.canister_id]) + if S.memory_allocation[A.canister_id] > 0: - memory_usage_wasm_state(New_state) + - memory_usage_raw_module(A.wasm_module) + - memory_usage_canister_history(New_canister_history) + - memory_usage_chunk_store(New_chunk_store) + - memory_usage_snapshot(S.snapshots[A.canister_id]) ≤ S.memory_allocation[A.canister_id] + Total_memory_usage ≤ S.memory_allocation[A.canister_id] (S.wasm_memory_limit[A.canister_id] = 0) or |New_state.store.mem| <= S.wasm_memory_limit[A.canister_id] @@ -5002,12 +5140,14 @@ liquid_balance(S, A.canister_id) ≥ MAX_CYCLES_PER_MESSAGE liquid_balance(S', A.canister_id) ≥ 0 +Total_memory_usage = memory_usage_wasm_state(New_state) + + memory_usage_raw_module(A.wasm_module) + + memory_usage_canister_history(New_canister_history) + + memory_usage_chunk_store(S.chunk_store[A.canister_id]) + + memory_usage_snapshot(S.snapshots[A.canister_id]) + if S.memory_allocation[A.canister_id] > 0: - memory_usage_wasm_state(New_state) + - memory_usage_raw_module(A.wasm_module) + - memory_usage_canister_history(New_canister_history) + - memory_usage_chunk_store(S[A.canister_id].chunk_store) + - memory_usage_snapshot(S.snapshots[A.canister_id]) ≤ S.memory_allocation[A.canister_id] + Total_memory_usage ≤ S.memory_allocation[A.canister_id] (S.wasm_memory_limit[A.canister_id] = 0) or |New_state.store.mem| <= S.wasm_memory_limit[A.canister_id] @@ -5424,6 +5564,8 @@ S with reserved_balances[A.canister_id] = (deleted) reserved_balance_limits[A.canister_id] = (deleted) wasm_memory_limit[A.canister_id] = (deleted) + wasm_memory_threshold[A.canister_id] = (deleted) + on_low_wasm_memory_hook_status[A.canister_id] = (deleted) certified_data[A.canister_id] = (deleted) canister_history[A.canister_id] = (deleted) canister_log_visibility[A.canister_id] = (deleted) @@ -5613,6 +5755,10 @@ if A.settings.wasm_memory_limit is not null: New_wasm_memory_limit = A.settings.wasm_memory_limit else: New_wasm_memory_limit = 0 +if A.settings.wasm_memory_threshold is not null: + New_wasm_memory_threshold = A.settings.wasm_memory_threshold +else: + New_wasm_memory_threshold = 0 Cycles_reserved = cycles_to_reserve(S, Canister_id, New_compute_allocation, New_memory_allocation, null, EmptyCanister.wasm_state) if A.amount is not null: @@ -5660,6 +5806,8 @@ S' = S with reserved_balances[Canister_id] = New_reserved_balance reserved_balance_limits[Canister_id] = New_reserved_balance_limit wasm_memory_limit[Canister_id] = New_wasm_memory_limit + wasm_memory_threshold[Canister_id] = New_wasm_memory_threshold + on_low_wasm_memory_hook_status[Canister_id] = ConditionNotSatisfied certified_data[Canister_id] = "" canister_history[Canister_id] = New_canister_history canister_log_visibility[Canister_id] = New_canister_log_visibility @@ -6973,6 +7121,32 @@ global_timer = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} ``` +- The function `on_low_wasm_memory` of the `CanisterModule` is defined if the WebAssembly program exports a function `func` named `canister_on_low_wasm_memory`, and has value + ``` + on_low_wasm_memory = λ (sysenv) → λ wasm_state → + let es = ref {empty_execution_state with + wasm_state = wasm_state; + params = empty_params with { arg = NoArg; caller = ic_principal; sysenv } + balance = sysenv.balance + context = T + } + try func() with Trap then Trap {cycles_used = es.cycles_used;} + discard_pending_call() + Return { + new_state = es.wasm_state; + new_calls = es.calls; + new_certified_data = es.certified_data; + new_global_timer = es.new_global_timer; + cycles_used = es.cycles_used; + } + ``` + + otherwise it is + + ```html + on_low_wasm_memory = λ (sysenv) → λ wasm_state → Trap {cycles_used = 0;} + ``` + - The function `callbacks` of the `CanisterModule` is defined as follows ``` I ∈ {i32, i64}