Skip to content

Motoko low_wasm_memory example #1139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
/motoko/internet_identity_integration/ @dfinity/identity
/motoko/life/ @dfinity/languages
/motoko/llm_chatbot/ @dfinity/ninja-devs
/motoko/low_wasm_memory/ @dfinity/languages
/motoko/minimal-counter-dapp/ @dfinity/growth
/motoko/parallel_calls/ @dfinity/languages
/motoko/pub-sub/ @dfinity/growth
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/motoko-low_wasm_memory-example.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: motoko-low_wasm_memory
on:
push:
branches:
- master
pull_request:
paths:
- motoko/low_wasm_memory/**
- .github/workflows/provision-darwin.sh
- .github/workflows/provision-linux.sh
- .github/workflows/motoko-low_wasm_memory-example.yml
- .github/workflows/motoko-low_wasm_memory-skip.yml
- .ic-commit
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
motoko-low_wasm_memory-darwin:
runs-on: macos-15
steps:
- uses: actions/checkout@v1
- name: Provision Darwin
run: bash .github/workflows/provision-darwin.sh
- name: Motoko low_wasm_memory Darwin
run: |
set -euo pipefail
pushd motoko/low_wasm_memory
dfx start --background
dfx deploy low_wasm_memory_hook
dfx canister update-settings low_wasm_memory_hook --wasm-memory-limit 4000000 --wasm-memory-threshold 1000000
dfx canister status low_wasm_memory_hook
max_wait=20
waited=0
until [[ $(dfx canister call low_wasm_memory_hook --query getExecutedFunctionsOrder) == *onLowWasmMemory* ]]; do
sleep 1
waited=$((waited+1))
if [ $waited -ge $max_wait ]; then
echo "Timed out waiting for onLowWasmMemory event"
exit 1
fi
done
popd
motoko-low_wasm_memory-linux:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v1
- name: Provision Linux
run: bash .github/workflows/provision-linux.sh
- name: Motoko low_wasm_memory Linux
run: |
set -euo pipefail
pushd motoko/low_wasm_memory
dfx start --background
dfx deploy low_wasm_memory_hook
dfx canister update-settings low_wasm_memory_hook --wasm-memory-limit 4000000 --wasm-memory-threshold 1000000
dfx canister status low_wasm_memory_hook
max_wait=20
waited=0
until [[ $(dfx canister call low_wasm_memory_hook --query getExecutedFunctionsOrder) == *onLowWasmMemory* ]]; do
sleep 1
waited=$((waited+1))
if [ $waited -ge $max_wait ]; then
echo "Timed out waiting for onLowWasmMemory event"
exit 1
fi
done
popd
20 changes: 18 additions & 2 deletions .github/workflows/rust-low_wasm_memory-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,21 @@ jobs:
run: bash .github/workflows/provision-darwin.sh
- name: Rust low_wasm_memory Darwin
run: |
set -euo pipefail
pushd rust/low_wasm_memory
dfx start --background
dfx deploy low_wasm_memory_hook
dfx canister update-settings low_wasm_memory_hook --wasm-memory-limit 3000000 --wasm-memory-threshold 2000000
dfx canister status low_wasm_memory_hook
dfx canister call low_wasm_memory_hook --query get_executed_functions_order | grep -q 'OnLowWasmMemory' || while ! dfx canister call low_wasm_memory_hook --query get_executed_functions_order | grep -q 'OnLowWasmMemory'; do
max_wait=20
waited=0
until [[ $(dfx canister call low_wasm_memory_hook --query get_executed_functions_order) == *OnLowWasmMemory* ]]; do
sleep 1
waited=$((waited+1))
if [ $waited -ge $max_wait ]; then
echo "Timed out waiting for OnLowWasmMemory event"
exit 1
fi
done
popd
rust-low_wasm_memory-linux:
Expand All @@ -40,12 +48,20 @@ jobs:
run: bash .github/workflows/provision-linux.sh
- name: Rust low_wasm_memory Linux
run: |
set -euo pipefail
pushd rust/low_wasm_memory
dfx start --background
dfx deploy low_wasm_memory_hook
dfx canister update-settings low_wasm_memory_hook --wasm-memory-limit 3000000 --wasm-memory-threshold 2000000
dfx canister status low_wasm_memory_hook
dfx canister call low_wasm_memory_hook --query get_executed_functions_order | grep -q 'OnLowWasmMemory' || while ! dfx canister call low_wasm_memory_hook --query get_executed_functions_order | grep -q 'OnLowWasmMemory'; do
max_wait=20
waited=0
until [[ $(dfx canister call low_wasm_memory_hook --query get_executed_functions_order) == *OnLowWasmMemory* ]]; do
sleep 1
waited=$((waited+1))
if [ $waited -ge $max_wait ]; then
echo "Timed out waiting for OnLowWasmMemory event"
exit 1
fi
done
popd
165 changes: 165 additions & 0 deletions motoko/low_wasm_memory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Low Wasm memory hook

The Internet Computer can automatically execute a special type of function called low Wasm memory hook, which runs when the available Wasm memory of the canister falls below the 'wasm-memory-threshold'.

This example demonstrates the ways of using low Wasm memory hook on the Internet Computer.

The example consists of a canister named `low_wasm_memory_hook` implementing the functionality that it increases usage of Wasm memory in every 'heartbeat' execution, until the low Wasm memory hook is run.

## Prerequisites
This example requires an installation of:

- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/getting-started/install).
- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples`

## Example: Low Wasm memory hook

In this example, the canister will periodically increase its memory usage (as defined in the `heartbeat` function) until the low Wasm memory hook is run
when the memory usage exceeds the `wasm-memory-threshold`.

- ### Step 1: Setup project environment

Navigate into the folder containing the project's files and start a local instance of the replica with the command:

```sh
cd examples/motoko/low_wasm_memory
dfx start --clean
```

This terminal will stay blocked, printing log messages, until the `Ctrl+C` is pressed or `dfx stop` command is run.

- ### Step 2: Open another terminal window in the same directory:

```sh
cd examples/motoko/low_wasm_memory
```

- ### Step 3: Create a new canister

```sh
dfx canister create low_wasm_memory_hook
```

Example output:

```sh
Created a wallet canister on the "local" network for user "default" with ID "uqqxf-5h777-77774-qaaaa-cai"
low_wasm_memory_hook canister created with canister id: uxrrr-q7777-77774-qaaaq-cai
```

- ### Step 4: Update canister settings

Update canister settings:

```sh
dfx canister update-settings low_wasm_memory_hook --wasm-memory-limit 4000000 --wasm-memory-threshold 1000000
```

and test that setting are correctly updated:

```sh
dfx canister status low_wasm_memory_hook
```

Example output:

```sh
% dfx canister status low_wasm_memory_hook
Canister status call result for low_wasm_memory_hook.
Status: Running
Controllers: 3apfx-fn75o-xtwmf-svjzg-hu5xt-bg2dr-ubczq-uhvlq-2a5gf-ya4fn-dqe uqqxf-5h777-77774-qaaaa-cai
Memory allocation: 0 Bytes
Compute allocation: 0 %
Freezing threshold: 2_592_000 Seconds
Idle cycles burned per day: 1_757 Cycles
Memory Size: 172 Bytes
Balance: 2_999_997_206_000 Cycles
Reserved: 0 Cycles
Reserved cycles limit: 5_000_000_000_000 Cycles
Wasm memory limit: 4_000_000 Bytes
Wasm memory threshold: 1_000_000 Bytes
Module hash: None
Number of queries: 0
Instructions spent in queries: 0
Total query request payload size: 0 Bytes
Total query response payload size: 0 Bytes
Log visibility: controllers
```

In the example above we set the 'wasm-memory-limit' to 4MB and 'wasm-memory-threshold' to 1MB. Hence whenever the Wasm memory
used by the canister is above 3MB (in other words, the remaining Wasm memory is less than 'wasm-memory-threshold')
the low Wasm memory hook will run.

Notice that the current Memory Size is low, because the canister is not yet deployed.

- ### Step 5: Compile and deploy the `low_wasm_memory_hook` canister:

```sh
dfx deploy low_wasm_memory_hook
```

Example output:

```sh
% dfx deploy low_wasm_memory_hook
Deploying: low_wasm_memory_hook
All canisters have already been created.
Building canister 'low_wasm_memory_hook'.
Installed code for canister low_wasm_memory_hook, with canister ID uxrrr-q7777-77774-qaaaq-cai
Deployed canisters.
URLs:
Backend canister via Candid interface:
low_wasm_memory_hook: http://127.0.0.1:4943/?canisterId=u6s2n-gx777-77774-qaaba-cai&id=uxrrr-q7777-77774-qaaaq-cai
```

After the deployment, the memory usage periodically increases as defined in the `heartbeat` function.
You can observe the Memory Size with the following command:

```sh
dfx canister status low_wasm_memory_hook | grep 'Memory Size'
```

- ### Step 6: After 10s, observe the output of the `getExecutedFunctionsOrder` query:

Query the canister calling 'getExecutedFunctionsOrder' to get the order of executed functions.

```sh
dfx canister call low_wasm_memory_hook --query getExecutedFunctionsOrder
```

Repeat the call until the last executed method is 'onLowWasmMemory' hook.

Example output:

```sh
% dfx canister call low_wasm_memory_hook --query getExecutedFunctionsOrder
(
vec { variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { heartbeat }; variant { onLowWasmMemory };},
)
```

To repeat the example, you can redeploy the canister and reset its state, with the following command:

```sh
dfx deploy low_wasm_memory_hook --mode reinstall
```

## Further learning
1. Have a look at the locally running dashboard. The URL is at the end of the `dfx start` command: `Dashboard: http://localhost/...`
2. Check out `low_wasm_memory_hook` canisters Candid user interface. The URLs are at the end of the `dfx deploy` command: `low_wasm_memory_hook: http://127.0.0.1/...`

### Canister interface

The `low_wasm_memory_hook` canister provide the following interface:
* `getExecutedFunctionsOrder` ; returns the vector with values of `FnType` (a variant with `#heartbeat` or `#onLowWasmMemory` ) representing the order of functions executed.

Example usage:
```sh
dfx canister call low_wasm_memory_hook --query getExecutedFunctionsOrder
```

## Conclusion
For more information take a look at [low Wasm memory hook specification](https://internetcomputer.org/docs/references/ic-interface-spec#on-low-wasm-memory).

## Security considerations and best practices
If you base your application on this example, we recommend you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/current/references/security/) for developing on the Internet Computer. This example may not implement all the best practices.
16 changes: 16 additions & 0 deletions motoko/low_wasm_memory/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"canisters": {
"low_wasm_memory_hook": {
"main": "src/low_wasm_memory_hook/main.mo",
"type": "motoko"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
37 changes: 37 additions & 0 deletions motoko/low_wasm_memory/src/low_wasm_memory_hook/main.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Array "mo:base/Array";
import Deque "mo:base/Deque";
import Buffer "mo:base/Buffer";

actor {
// Types
type FnType = {
#heartbeat;
#onLowWasmMemory;
};

// State
var fnOrderBuffer = Buffer.Buffer<FnType>(30);
var bytes : Deque.Deque<[Nat]> = Deque.empty();
var hookExecuted : Bool = false;

// Query function to get execution order
public query func getExecutedFunctionsOrder() : async [FnType] {
Buffer.toArray(fnOrderBuffer);
};

// Heartbeat function that increases memory usage
system func heartbeat() : async () {
if (not hookExecuted) {
fnOrderBuffer.add(#heartbeat);
// Allocate more memory by creating a large array
let chunk = Array.tabulate<Nat>(10_000, func _ = 0);
bytes := Deque.pushBack(bytes, chunk);
};
};

// Low WASM memory hook
system func lowmemory() : async* () {
hookExecuted := true;
fnOrderBuffer.add(#onLowWasmMemory);
};
};
Loading