diff --git a/Changelog.md b/Changelog.md index e1ef818826f..af02a7981a1 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,9 +4,9 @@ * motoko (`moc`) - * bugfix: Some valid upgrades deleting a stable variable could fail the `--enhanced-orthogonal-persistence` stable compatibility check due to a bug (#4855). - - * Breaking change (minor): + * Support low Wasm memory hook: `system func lowmemory() : async* () { ... }` (#4849). + + * Breaking change (minor) (#4854): * For enhanced orthogonal persistence: The Wasm persistence modes used internally for canister upgrades have been changed to lower case names, `keep` and `replace` and instead of `Keep` and `Replace`: @@ -14,11 +14,13 @@ If using actor class instances with enhanced orthogonal persistence, you would need to recompile the program and upgrade with latest `moc` and `dfx`. Otherwise, no action is needed. + * bugfix: Some valid upgrades deleting a stable variable could fail the `--enhanced-orthogonal-persistence` stable compatibility check due to a bug (#4855). + ## 0.13.5 (2024-12-06) * motoko (`moc`) - * Breaking change (minor): + * Breaking change (minor) (#4786): * Add new keyword `transient` with exactly the same meaning as the old keyword `flexible` (but a more familiar reading). @@ -28,10 +30,8 @@ `let` or `var` declaration is `stable` (not `flexible` or `transient`). For example, a stateful counter can now be declared as: - ``` motoko persistent actor { - // counts increments since last upgrade transient var invocations = 0; @@ -42,10 +42,8 @@ value += 1; invocations += 1; } - } ``` - On upgrade, the transient variable `invocations` will be reset to `0` and `value`, now implicitly `stable`, will retain its current value. Legacy actors and classes declared without the `persistent` keyword have the same semantics as before. diff --git a/doc/md/canister-maintenance/memory.md b/doc/md/canister-maintenance/memory.md new file mode 100644 index 00000000000..5294c5b2d44 --- /dev/null +++ b/doc/md/canister-maintenance/memory.md @@ -0,0 +1,26 @@ +--- +sidebar_position: 5 +--- + +# Memory diagnostics + +## Low memory hook + +The IC allows to implement a low memory hook, which is a warning trigger when main memory is becoming scarce. + +For this purpose, a Motoko actor or actor class instance can implement the system function `lowmemory()`. This system function is scheduled when canister's free main memory space has fallen below the defined threshold `wasm_memory_threshold`, that is is part of the canister settings. In Motoko, `lowmemory()` implements the `canister_on_low_wasm_memory` hook defined in the IC specification. + +Example of using the low memory hook: +``` +actor { + system func lowmemory() : async* () { + Debug.print("Low memory!"); + } +} +``` + +The following properties apply to the low memory hook: +* The execution of `lowmemory` happens with a certain delay, as it is scheduled as a separate asynchronous message that runs after the message in which the threshold was crossed. +* Once executed, `lowmemory` is only triggered again when the main memory free space first exceeds and then falls below the threshold. +* Traps or unhandled errors in `lowmemory` are ignored. Traps only revert the changes done in `lowmemory`. +* Due to its `async*` return type, the `lowmemory` function may send further messages and `await` results. diff --git a/nix/sources.json b/nix/sources.json index 92dade76e8c..86e6de79a04 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -21,15 +21,15 @@ "version": "3.2.25" }, "ic": { - "branch": "luc/lower-case-persistence-modes", + "branch": "luc/latest-drun", "description": "Internet Computer blockchain source: the client/replica software run by nodes", "homepage": "", "owner": "luc-blaeser", "repo": "ic", - "rev": "48ba595fb1a6bc828e7e9dae976db42b5526a5b6", - "sha256": "0136hxidak4qsk0nhjdy2nmciwb0lprm07vs117xdw3cp4m6py2z", + "rev": "01c8dfda47126bb5f688302e0161f17c2c0cfe6f", + "sha256": "16h037xagxwf00k13y3h0gvfs7f3c45z2ml0j1pgvzg2fx5pzahy", "type": "tarball", - "url": "https://github.com/luc-blaeser/ic/archive/48ba595fb1a6bc828e7e9dae976db42b5526a5b6.tar.gz", + "url": "https://github.com/luc-blaeser/ic/archive/01c8dfda47126bb5f688302e0161f17c2c0cfe6f.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "ic-hs": { diff --git a/src/codegen/compile_classical.ml b/src/codegen/compile_classical.ml index a16282ca4db..3b6a6105c48 100644 --- a/src/codegen/compile_classical.ml +++ b/src/codegen/compile_classical.ml @@ -5277,6 +5277,18 @@ module IC = struct edesc = nr (FuncExport (nr fi)) }) + let export_low_memory env = + assert (E.mode env = Flags.ICMode || E.mode env = Flags.RefMode); + let fi = E.add_fun env "canister_on_low_wasm_memory" + (Func.of_body env [] [] (fun env -> + G.i (Call (nr (E.built_in env "low_memory_exp"))) ^^ + GC.collect_garbage env)) + in + E.add_export env (nr { + name = Lib.Utf8.decode "canister_on_low_wasm_memory"; + edesc = nr (FuncExport (nr fi)) + }) + let export_wasi_start env = assert (E.mode env = Flags.WASIMode); let fi = E.add_fun env "_start" (Func.of_body env [] [] (fun env1 -> @@ -13014,6 +13026,15 @@ and main_actor as_opt mod_env ds fs up = IC.export_inspect env; end; + (* Export low memory hook (but only when required) *) + begin match up.low_memory.it with + | Ir.PrimE (Ir.TupPrim, []) -> () + | _ -> + Func.define_built_in env "low_memory_exp" [] [] (fun env -> + compile_exp_as env ae2 SR.unit up.low_memory); + IC.export_low_memory env; + end; + (* Helper function to build the stable actor wrapper *) Func.define_built_in mod_env IC.get_actor_to_persist_function_name [] [I32Type] (fun env -> compile_exp_as env ae2 SR.Vanilla build_stable_actor diff --git a/src/codegen/compile_enhanced.ml b/src/codegen/compile_enhanced.ml index 61d21e839f5..0f98f94a28b 100644 --- a/src/codegen/compile_enhanced.ml +++ b/src/codegen/compile_enhanced.ml @@ -4972,6 +4972,18 @@ module IC = struct edesc = nr (FuncExport (nr fi)) }) + let export_low_memory env = + assert (E.mode env = Flags.ICMode || E.mode env = Flags.RefMode); + let fi = E.add_fun env "canister_on_low_wasm_memory" + (Func.of_body env [] [] (fun env -> + G.i (Call (nr (E.built_in env "low_memory_exp"))) ^^ + GC.collect_garbage env)) + in + E.add_export env (nr { + name = Lib.Utf8.decode "canister_on_low_wasm_memory"; + edesc = nr (FuncExport (nr fi)) + }) + let initialize_main_actor_function_name = "@initialize_main_actor" let initialize_main_actor env = @@ -13106,6 +13118,15 @@ and main_actor as_opt mod_env ds fs up = IC.export_inspect env; end; + (* Export low memory hook (but only when required) *) + begin match up.low_memory.it with + | Ir.PrimE (Ir.TupPrim, []) -> () + | _ -> + Func.define_built_in env "low_memory_exp" [] [] (fun env -> + compile_exp_as env ae2 SR.unit up.low_memory); + IC.export_low_memory env; + end; + (* Helper function to build the stable actor wrapper *) Func.define_built_in mod_env IC.get_actor_to_persist_function_name [] [I64Type] (fun env -> compile_exp_as env ae2 SR.Vanilla build_stable_actor diff --git a/src/ir_def/arrange_ir.ml b/src/ir_def/arrange_ir.ml index b0f110029fb..55a42d9cec9 100644 --- a/src/ir_def/arrange_ir.ml +++ b/src/ir_def/arrange_ir.ml @@ -35,13 +35,14 @@ let rec exp e = match e.it with | TryE (e, cs, None) -> "TryE" $$ [exp e] @ List.map case cs | TryE (e, cs, Some (i, _)) -> "TryE" $$ [exp e] @ List.map case cs @ Atom ";" :: [id i] -and system { meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type} = (* TODO: show meta? *) +and system { meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type} = (* TODO: show meta? *) "System" $$ [ "Pre" $$ [exp preupgrade]; "Post" $$ [exp postupgrade]; "Heartbeat" $$ [exp heartbeat]; "Timer" $$ [exp timer]; "Inspect" $$ [exp inspect]; + "LowMemory" $$ [exp low_memory]; "StableRecord" $$ [exp stable_record]; "StableType" $$ [typ stable_type] ] diff --git a/src/ir_def/check_ir.ml b/src/ir_def/check_ir.ml index 37e84da5623..8d8d23c56d9 100644 --- a/src/ir_def/check_ir.ml +++ b/src/ir_def/check_ir.ml @@ -817,7 +817,7 @@ let rec check_exp env (exp:Ir.exp) : unit = typ exp_r <: T.(Construct.err_contT unit); typ exp_c <: Construct.clean_contT; | ActorE (ds, fs, - { preupgrade; postupgrade; meta; heartbeat; timer; inspect; stable_record; stable_type }, t0) -> + { preupgrade; postupgrade; meta; heartbeat; timer; inspect; low_memory; stable_record; stable_type }, t0) -> (* TODO: check meta *) let env' = { env with async = None } in let scope1 = gather_block_decs env' ds in @@ -828,12 +828,15 @@ let rec check_exp env (exp:Ir.exp) : unit = check_exp env'' heartbeat; check_exp env'' timer; check_exp env'' inspect; + let async_cap = Some Async_cap.top_cap in + check_exp { env'' with async = async_cap } low_memory; check_exp env'' stable_record; typ preupgrade <: T.unit; typ postupgrade <: T.unit; typ heartbeat <: T.unit; typ timer <: T.unit; typ inspect <: T.unit; + typ low_memory <: T.unit; typ stable_record <: stable_type; check (T.is_obj t0) "bad annotation (object type expected)"; let (s0, tfs0) = T.as_obj t0 in @@ -1160,7 +1163,7 @@ let check_comp_unit env = function let env' = adjoin env scope in check_decs env' ds | ActorU (as_opt, ds, fs, - { preupgrade; postupgrade; meta; heartbeat; timer; inspect; stable_type; stable_record }, t0) -> + { preupgrade; postupgrade; meta; heartbeat; timer; inspect; low_memory; stable_type; stable_record }, t0) -> let check p = check env no_region p in let (<:) t1 t2 = check_sub env no_region t1 t2 in let env' = match as_opt with @@ -1179,11 +1182,14 @@ let check_comp_unit env = function check_exp env'' timer; check_exp env'' inspect; check_exp env'' stable_record; + let async_cap = Some Async_cap.top_cap in + check_exp { env'' with async = async_cap } low_memory; typ preupgrade <: T.unit; typ postupgrade <: T.unit; typ heartbeat <: T.unit; typ timer <: T.unit; typ inspect <: T.unit; + typ low_memory <: T.unit; typ stable_record <: stable_type; check (T.is_obj t0) "bad annotation (object type expected)"; let (s0, tfs0) = T.as_obj t0 in diff --git a/src/ir_def/freevars.ml b/src/ir_def/freevars.ml index ae8d8309cf5..374053af34d 100644 --- a/src/ir_def/freevars.ml +++ b/src/ir_def/freevars.ml @@ -123,12 +123,13 @@ let rec exp e : f = match e.it with and actor ds fs u = close (decs ds +++ fields fs +++ system u) -and system {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; _} = +and system {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; _} = under_lambda (exp preupgrade) ++ under_lambda (exp postupgrade) ++ under_lambda (exp heartbeat) ++ under_lambda (exp timer) ++ under_lambda (exp inspect) ++ + under_lambda (exp low_memory) ++ under_lambda (exp stable_record) and exps es : f = unions exp es diff --git a/src/ir_def/ir.ml b/src/ir_def/ir.ml index 192a7453446..d2fa9a1f79a 100644 --- a/src/ir_def/ir.ml +++ b/src/ir_def/ir.ml @@ -85,6 +85,7 @@ and system = { heartbeat : exp; timer : exp; (* TODO: use an option type: (Default of exp | UserDefined of exp) option *) inspect : exp; + low_memory : exp; stable_record: exp; stable_type: Type.typ; } diff --git a/src/ir_def/rename.ml b/src/ir_def/rename.ml index f97594fba41..7cb2dd3431b 100644 --- a/src/ir_def/rename.ml +++ b/src/ir_def/rename.ml @@ -33,7 +33,7 @@ and exp' rho = function | VarE (m, i) -> VarE (m, id rho i) | LitE _ as e -> e | PrimE (p, es) -> PrimE (prim rho p, List.map (exp rho) es) - | ActorE (ds, fs, { meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, t) -> + | ActorE (ds, fs, { meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, t) -> let ds', rho' = decs rho ds in ActorE (ds', @@ -44,6 +44,7 @@ and exp' rho = function heartbeat = exp rho' heartbeat; timer = exp rho' timer; inspect = exp rho' inspect; + low_memory = exp rho' low_memory; stable_type = stable_type; stable_record = exp rho' stable_record; }, @@ -200,7 +201,7 @@ let comp_unit rho cu = match cu with | LibU (ds, e) -> let ds', rho' = decs rho ds in LibU (ds', exp rho' e) - | ActorU (as_opt, ds, fs, { meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type }, t) -> + | ActorU (as_opt, ds, fs, { meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type }, t) -> let as_opt', rho' = match as_opt with | None -> None, rho | Some as_ -> @@ -215,6 +216,7 @@ let comp_unit rho cu = match cu with heartbeat = exp rho'' heartbeat; timer = exp rho'' timer; inspect = exp rho'' inspect; + low_memory = exp rho'' low_memory; stable_record = exp rho'' stable_record; stable_type = stable_type; }, t) diff --git a/src/ir_passes/async.ml b/src/ir_passes/async.ml index b3b52989e7a..45ef1afea57 100644 --- a/src/ir_passes/async.ml +++ b/src/ir_passes/async.ml @@ -442,7 +442,7 @@ let transform prog = | (Returns | Replies), _ -> assert false end end - | ActorE (ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, typ) -> + | ActorE (ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, typ) -> ActorE (t_decs ds, t_fields fs, {meta; preupgrade = t_exp preupgrade; @@ -450,6 +450,7 @@ let transform prog = heartbeat = t_exp heartbeat; timer = t_exp timer; inspect = t_exp inspect; + low_memory = t_exp low_memory; stable_record = t_exp stable_record; stable_type = t_typ stable_type; }, @@ -523,7 +524,7 @@ let transform prog = and t_comp_unit = function | LibU _ -> raise (Invalid_argument "cannot compile library") | ProgU ds -> ProgU (t_decs ds) - | ActorU (args_opt, ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, t) -> + | ActorU (args_opt, ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, t) -> ActorU (Option.map t_args args_opt, t_decs ds, t_fields fs, { meta; preupgrade = t_exp preupgrade; @@ -531,6 +532,7 @@ let transform prog = heartbeat = t_exp heartbeat; timer = t_exp timer; inspect = t_exp inspect; + low_memory = t_exp low_memory; stable_record = t_exp stable_record; stable_type = t_typ stable_type; }, diff --git a/src/ir_passes/await.ml b/src/ir_passes/await.ml index 410776ca813..c3896cbbfac 100644 --- a/src/ir_passes/await.ml +++ b/src/ir_passes/await.ml @@ -181,7 +181,7 @@ and t_exp' context exp = assert (not (T.is_shared_func (typ exp))); let context' = LabelEnv.singleton Return Label in FuncE (x, s, c, typbinds, pat, typs, t_exp context' exp1) - | ActorE (ds, ids, { meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, t) -> + | ActorE (ds, ids, { meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, t) -> ActorE (t_decs context ds, ids, { meta; preupgrade = t_exp LabelEnv.empty preupgrade; @@ -189,6 +189,7 @@ and t_exp' context exp = heartbeat = t_ignore_throw LabelEnv.empty heartbeat; timer = t_ignore_throw LabelEnv.empty timer; inspect = t_exp LabelEnv.empty inspect; + low_memory = t_ignore_throw LabelEnv.empty low_memory; stable_record = t_exp LabelEnv.empty stable_record; stable_type; }, @@ -647,7 +648,7 @@ and t_comp_unit context = function expD (c_block context' ds (tupE []) (meta (T.unit) (fun v1 -> tupE []))) ] end - | ActorU (as_opt, ds, ids, { meta = m; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, t) -> + | ActorU (as_opt, ds, ids, { meta = m; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, t) -> ActorU (as_opt, t_decs context ds, ids, { meta = m; preupgrade = t_exp LabelEnv.empty preupgrade; @@ -655,6 +656,7 @@ and t_comp_unit context = function heartbeat = t_ignore_throw LabelEnv.empty heartbeat; timer = t_timer_throw LabelEnv.empty timer; inspect = t_exp LabelEnv.empty inspect; + low_memory = t_ignore_throw LabelEnv.empty low_memory; stable_record = t_exp LabelEnv.empty stable_record; stable_type; }, diff --git a/src/ir_passes/const.ml b/src/ir_passes/const.ml index 9bc4591cad1..e62e23ef575 100644 --- a/src/ir_passes/const.ml +++ b/src/ir_passes/const.ml @@ -164,7 +164,7 @@ let rec exp lvl (env : env) e : Lbool.t = surely_false | NewObjE _ -> (* mutable objects *) surely_false - | ActorE (ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, _typ) -> + | ActorE (ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, _typ) -> (* this may well be “the” top-level actor, so don’t update lvl here *) let (env', _) = decs lvl env ds in exp_ lvl env' preupgrade; @@ -172,6 +172,7 @@ let rec exp lvl (env : env) e : Lbool.t = exp_ lvl env' heartbeat; exp_ lvl env' timer; exp_ lvl env' inspect; + exp_ lvl env' low_memory; exp_ lvl env' stable_record; surely_false in @@ -228,7 +229,7 @@ and block lvl env (ds, body) = and comp_unit = function | LibU _ -> raise (Invalid_argument "cannot compile library") | ProgU ds -> decs_ TopLvl M.empty ds - | ActorU (as_opt, ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, typ) -> + | ActorU (as_opt, ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, typ) -> let env = match as_opt with | None -> M.empty | Some as_ -> args TopLvl M.empty as_ @@ -239,6 +240,7 @@ and comp_unit = function exp_ TopLvl env' heartbeat; exp_ TopLvl env' timer; exp_ TopLvl env' inspect; + exp_ TopLvl env' low_memory; exp_ TopLvl env' stable_record let analyze ((cu, _flavor) : prog) = diff --git a/src/ir_passes/eq.ml b/src/ir_passes/eq.ml index 52486abcf4b..5987e11a3fc 100644 --- a/src/ir_passes/eq.ml +++ b/src/ir_passes/eq.ml @@ -249,7 +249,7 @@ and t_exp' env = function NewObjE (sort, ids, t) | SelfCallE (ts, e1, e2, e3, e4) -> SelfCallE (ts, t_exp env e1, t_exp env e2, t_exp env e3, t_exp env e4) - | ActorE (ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, typ) -> + | ActorE (ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, typ) -> (* Until Actor expressions become their own units, we repeat what we do in `comp_unit` below *) let env1 = empty_env () in @@ -259,6 +259,7 @@ and t_exp' env = function let heartbeat' = t_exp env1 heartbeat in let timer' = t_exp env1 timer in let inspect' = t_exp env1 inspect in + let low_memory' = t_exp env1 low_memory in let stable_record' = t_exp env1 stable_record in let decls = eq_decls !(env1.params) in ActorE (decls @ ds', fields, @@ -268,6 +269,7 @@ and t_exp' env = function heartbeat = heartbeat'; timer = timer'; inspect = inspect'; + low_memory = low_memory'; stable_record = stable_record'; stable_type; }, @@ -301,7 +303,7 @@ and t_comp_unit = function let ds' = t_decs env ds in let decls = eq_decls !(env.params) in ProgU (decls @ ds') - | ActorU (as_opt, ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, typ) -> + | ActorU (as_opt, ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, typ) -> let env = empty_env () in let ds' = t_decs env ds in let preupgrade' = t_exp env preupgrade in @@ -309,6 +311,7 @@ and t_comp_unit = function let heartbeat' = t_exp env heartbeat in let timer' = t_exp env timer in let inspect' = t_exp env inspect in + let low_memory' = t_exp env low_memory in let stable_record' = t_exp env stable_record in let decls = eq_decls !(env.params) in ActorU (as_opt, decls @ ds', fields, @@ -318,6 +321,7 @@ and t_comp_unit = function heartbeat = heartbeat'; timer = timer'; inspect = inspect'; + low_memory = low_memory'; stable_record = stable_record'; stable_type; }, typ) diff --git a/src/ir_passes/erase_typ_field.ml b/src/ir_passes/erase_typ_field.ml index 2b5daf75d70..f569f0ee765 100644 --- a/src/ir_passes/erase_typ_field.ml +++ b/src/ir_passes/erase_typ_field.ml @@ -126,7 +126,7 @@ let transform prog = DefineE (id, mut, t_exp exp1) | FuncE (x, s, c, typbinds, args, ret_tys, exp) -> FuncE (x, s, c, t_typ_binds typbinds, t_args args, List.map t_typ ret_tys, t_exp exp) - | ActorE (ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, typ) -> + | ActorE (ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, typ) -> ActorE (t_decs ds, t_fields fs, {meta; preupgrade = t_exp preupgrade; @@ -134,6 +134,7 @@ let transform prog = heartbeat = t_exp heartbeat; timer = t_exp timer; inspect = t_exp inspect; + low_memory = t_exp low_memory; stable_record = t_exp stable_record; stable_type = t_typ stable_type; }, @@ -211,7 +212,7 @@ let transform prog = and t_comp_unit = function | LibU _ -> raise (Invalid_argument "cannot compile library") | ProgU ds -> ProgU (t_decs ds) - | ActorU (args_opt, ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, t) -> + | ActorU (args_opt, ds, fs, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, t) -> ActorU (Option.map t_args args_opt, t_decs ds, t_fields fs, { meta; preupgrade = t_exp preupgrade; @@ -219,6 +220,7 @@ let transform prog = heartbeat = t_exp heartbeat; timer = t_exp timer; inspect = t_exp inspect; + low_memory = t_exp low_memory; stable_record = t_exp stable_record; stable_type = t_typ stable_type; }, diff --git a/src/ir_passes/show.ml b/src/ir_passes/show.ml index 23f195220d7..132d1e6d5bc 100644 --- a/src/ir_passes/show.ml +++ b/src/ir_passes/show.ml @@ -291,7 +291,7 @@ and t_exp' env = function NewObjE (sort, ids, t) | SelfCallE (ts, e1, e2, e3, e4) -> SelfCallE (ts, t_exp env e1, t_exp env e2, t_exp env e3, t_exp env e4) - | ActorE (ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, typ) -> + | ActorE (ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, typ) -> (* Until Actor expressions become their own units, we repeat what we do in `comp_unit` below *) let env1 = empty_env () in @@ -301,6 +301,7 @@ and t_exp' env = function let heartbeat' = t_exp env1 heartbeat in let timer' = t_exp env1 timer in let inspect' = t_exp env1 inspect in + let low_memory' = t_exp env1 low_memory in let stable_record' = t_exp env1 stable_record in let decls = show_decls !(env1.params) in ActorE (decls @ ds', fields, @@ -310,6 +311,7 @@ and t_exp' env = function heartbeat = heartbeat'; timer = timer'; inspect = inspect'; + low_memory = low_memory'; stable_record = stable_record'; stable_type; }, @@ -342,7 +344,7 @@ and t_comp_unit = function let ds' = t_decs env ds in let decls = show_decls !(env.params) in ProgU (decls @ ds') - | ActorU (as_opt, ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; stable_record; stable_type}, typ) -> + | ActorU (as_opt, ds, fields, {meta; preupgrade; postupgrade; heartbeat; timer; inspect; low_memory; stable_record; stable_type}, typ) -> let env = empty_env () in let ds' = t_decs env ds in let preupgrade' = t_exp env preupgrade in @@ -350,6 +352,7 @@ and t_comp_unit = function let heartbeat' = t_exp env heartbeat in let timer' = t_exp env timer in let inspect' = t_exp env inspect in + let low_memory' = t_exp env low_memory in let stable_record' = t_exp env stable_record in let decls = show_decls !(env.params) in ActorU (as_opt, decls @ ds', fields, @@ -359,6 +362,7 @@ and t_comp_unit = function heartbeat = heartbeat'; timer = timer'; inspect = inspect'; + low_memory = low_memory'; stable_record = stable_record'; stable_type }, typ) diff --git a/src/lowering/desugar.ml b/src/lowering/desugar.ml index 59b3b6f8692..0400d4ab0b0 100644 --- a/src/lowering/desugar.ml +++ b/src/lowering/desugar.ml @@ -424,6 +424,9 @@ and call_system_func_opt name es obj_typ = (unitE ()) (primE (Ir.OtherPrim "trap") [textE "canister_inspect_message explicitly refused message"])) + | "lowmemory" -> + awaitE T.Cmp + (callE (varE (var id.it note)) [T.scope_bound] (unitE())) | name -> let inst = match name with | "preupgrade" | "postupgrade" -> [T.scope_bound] @@ -612,6 +615,10 @@ and build_actor at ts self_id es obj_typ = (match call_system_func_opt "inspect" es obj_typ with | Some call -> call | None -> tupE []); + low_memory = + (match call_system_func_opt "lowmemory" es obj_typ with + | Some call -> call + | None -> tupE []); stable_record = with_stable_vars (fun e -> e); stable_type = ty; }, diff --git a/src/mo_frontend/typing.ml b/src/mo_frontend/typing.ml index 06973302d4f..8bff7dacc23 100644 --- a/src/mo_frontend/typing.ml +++ b/src/mo_frontend/typing.ml @@ -392,6 +392,7 @@ let system_funcs tfs = ("timer", T.timer_type); T.("preupgrade", Func (Local, Returns, [scope_bind], [], [])); T.("postupgrade", Func (Local, Returns, [scope_bind], [], [])); + ("lowmemory", T.low_memory_type); ("inspect", (let msg_typ = T.decode_msg_typ tfs in let record_typ = diff --git a/src/mo_types/type.ml b/src/mo_types/type.ml index 6023cc4bdae..f46a9e79d0b 100644 --- a/src/mo_types/type.ml +++ b/src/mo_types/type.ml @@ -1354,6 +1354,8 @@ let timer_type = [global_timer_set_type], [Async (Fut, Var (default_scope_var, 0), unit)]) +let low_memory_type = + Func (Local, Returns, [scope_bind], [], [Async (Cmp, Var (default_scope_var, 0), unit)]) (* Well-known fields *) diff --git a/src/mo_types/type.mli b/src/mo_types/type.mli index 8498ac7e57f..f309dfc4502 100644 --- a/src/mo_types/type.mli +++ b/src/mo_types/type.mli @@ -100,6 +100,7 @@ val region : typ val heartbeat_type : typ val timer_type : typ val global_timer_set_type : typ +val low_memory_type : typ val sum : (lab * typ) list -> typ val obj : obj_sort -> (lab * typ) list -> typ diff --git a/test/fail/ok/M0129.tc.ok b/test/fail/ok/M0129.tc.ok index 14f666c0262..aafbcc41faf 100644 --- a/test/fail/ok/M0129.tc.ok +++ b/test/fail/ok/M0129.tc.ok @@ -1 +1 @@ -M0129.mo:2.15-2.21: type error [M0129], unexpected system method named foobar, expected heartbeat or timer or preupgrade or postupgrade or inspect +M0129.mo:2.15-2.21: type error [M0129], unexpected system method named foobar, expected heartbeat or timer or preupgrade or postupgrade or lowmemory or inspect diff --git a/test/run-drun/low-memory.mo b/test/run-drun/low-memory.mo new file mode 100644 index 00000000000..6b76c80ee8d --- /dev/null +++ b/test/run-drun/low-memory.mo @@ -0,0 +1,61 @@ +import Prim "mo:⛔"; +import LowMemoryActor "low-memory/low-memory-actor"; +import Cycles = "cycles/cycles"; + +actor Self { + type canister_settings = { + wasm_memory_threshold : ?Nat; + }; + + func setMemoryThreshold(a : actor {}, threshold : Nat) : async () { + let ic00 = actor "aaaaa-aa" : actor { + update_settings : shared { + canister_id : Principal; + settings : canister_settings; + } -> async (); + }; + let settings : canister_settings = { + wasm_memory_threshold = ?threshold; + }; + await ic00.update_settings({ + canister_id = Prim.principalOfActor(a); + settings; + }); + }; + + let kb = 1024; + let mb = 1024 * kb; + let gb = 1024 * mb; + let maxMemory = 4 * gb; // adjust when canister limits inspection is supported + + public shared func lowMemoryCallback() : async () { + Prim.debugPrint("Low memory callback"); + }; + + public shared func run() : async () { + Cycles.add(2_000_000_000_000); + let lowMemoryActor1 = await LowMemoryActor.LowMemoryActor(lowMemoryCallback); + // await lowMemoryActor1.allocateMemory(); + + let threshold1 = maxMemory - (await lowMemoryActor1.memorySize()) - mb : Nat; + await setMemoryThreshold(lowMemoryActor1, threshold1); + await lowMemoryActor1.allocateMemory(); + + // Not yet implemented on IC: Should retrigger when shrinking memory by reinstallation. + // let lowMemoryActor2 = await (system LowMemoryActor.LowMemoryActor)(#reinstall lowMemoryActor1)(lowMemoryCallback); + // await lowMemoryActor2.allocateMemory(); + // await lowMemoryActor2.allocateMemory(); + + // Not yet implemented on IC: Should retrigger when lowering threshold. + // let threshold2 = maxMemory - (await lowMemoryActor2.memorySize()) - mb : Nat; + // await setMemoryThreshold(lowMemoryActor2, threshold2); + // await lowMemoryActor2.allocateMemory(); + }; +}; + +//SKIP run +//SKIP run-low +//SKIP run-ir +//SKIP ic-ref-run + +//CALL ingress run "DIDL\x00\x00" diff --git a/test/run-drun/low-memory/low-memory-actor.mo b/test/run-drun/low-memory/low-memory-actor.mo new file mode 100644 index 00000000000..b059518bd4b --- /dev/null +++ b/test/run-drun/low-memory/low-memory-actor.mo @@ -0,0 +1,27 @@ +import Prim "mo:⛔"; + +actor class LowMemoryActor(callback : shared () -> async ()) { + system func lowmemory() : async* () { + Prim.debugPrint("Low memory!"); + await callback(); + Prim.debugPrint("Low memory callback done"); + }; + + type Node = { + array : [var Nat]; + next : ?Node; + }; + + var root : ?Node = null; + + public func allocateMemory() : async () { + let array = Prim.Array_init(8 * 1024 * 1024, 0); // 32 GB on 32-bit, 64 GB on 64-bit. + let node : Node = { array; next = root }; + root := ?node; + }; + + public func memorySize() : async Nat { + await async {}; // Allocate GC reserve (because of `--force-gc` flag during drun testing). + Prim.rts_memory_size(); + }; +}; diff --git a/test/run-drun/ok/low-memory.drun-run.ok b/test/run-drun/ok/low-memory.drun-run.ok new file mode 100644 index 00000000000..8727e74bd91 --- /dev/null +++ b/test/run-drun/ok/low-memory.drun-run.ok @@ -0,0 +1,6 @@ +ingress Completed: Reply: 0x4449444c016c01b3c4b1f204680100010a00000000000000000101 +ingress Completed: Reply: 0x4449444c0000 +debug.print: Low memory! +debug.print: Low memory callback +debug.print: Low memory callback done +ingress Completed: Reply: 0x4449444c0000