Beta Testing Motoko’s Enhanced Orthogonal Persistence (EOP)

EOP is a brand-new Motoko feature that has now been released for beta testing in the latest dfx 0.24.1-beta.0.

Motivation

EOP has been designed with the intention to relieve programmers from dealing with stable memory by offering an upgrade mechanism that is simple, safe, and scalable at the same time:

  • Simplicity: By way of orthogonal persistence (the stable variables in Motoko), transitively reachable structures of any first-order type are automatically persisted across upgrades. No stable memory or stable data structures are needed.
  • Safety: The runtime system rigorously checks type compatibility on upgrades and supports several kinds of data changes by implicit migration. Any more complex migration can be implemented by custom code. This prevents any data corruption or misinterpretation at the memory level.
  • Scalability: Upgrades become super-fast because the main memory is simply retained on an upgrade. No copying to and from stable memory is needed. The main memory has been extended to 64-bit to scale as large as stable memory in the future.

While orthogonal persistence (stable variables) was already available in previous Motoko versions, EOP significantly strengthens safety and scalability to make it the persistence model of choice. EOP is a feature that is unique to Motoko, as the language and runtime system have been specifically tailored to the properties of the IC.

Configuration

To activate enhanced orthogonal persistence under dfx (version dfx 0.24.1-beta.0 or higher), the following command-line argument needs to be specified in dfx.json:

{ 
  "main": "your-program.mo",
  "type" : "motoko",
  "args" : "--enhanced-orthogonal-persistence",
}

Temporary Limitations

EOP is rolled out in a risk-averse manner, and therefore first comes with limitations that will be relaxed in future:

  • Local testing: As of now, EOP does not yet run on the IC mainnet because 64-bit main memory is not yet enabled there. However, EOP can already be tested locally in dfx. IC support for Wasm Memory64 and thus EOP is expected to be soon available.
  • Limited memory: The IC will initially only offer a limited capacity of the 64-bit main memory, e.g. 4GB or 6GB. This will be gradually increased in the future to approach the same capacity as stable memory.

Other Implications

There are several other changes in behavior and runtime properties implied by the EOP that are worth to consider:

  • Upgrades become extremely fast, only depending on the number of types, not on the number of heap objects.

  • Upgrades will no longer hit the IC instruction limit, even for maximum heap usage.

  • Motoko automatically migrates from old 32-bit classical persistence to EOP. However, please note that the reverse direction, downgrading from EOP to 32-bit classical persistence, is not supported.

  • The change to 64-bit increases the memory demand on the heap, in the worst case by a factor of two. This means, while the main memory is still very limited in the initial beta testing phase, there will not be a gain in memory capacity. However, in the long term, 64-bit allows Motoko canisters to scale very large, without needing to change or upgrade the canister.

  • You should expect a moderate regression in terms of instruction costs of around 15% for normal execution due to combined related features, a new IC instruction cost model for Memory64, precise object tagging, change to incremental GC, and handling of compile-time-known data.

  • The garbage collector is fixed to the Incremental GC. This is because this GC is designed for scalability which is what we want to achieve with EOP.

  • Floating point formatting may slightly change: The debug print format of “not a number” is now NaN (originally nan or -nan). Float.format(#hex prec, x) is no longer supported which is probably not frequently used in practice.

  • With the introduction of 64-bit main memory, the charging of cycles per instruction may also be somewhat increased as a conservative operational measure during the testing phase and probably, also during the later production roll out - the rationale behind this will follow in the announcement when Memory64 support is enabled on the IC mainnet.

More Information

For more information, please see:

Contact

If you encounter any issues or have any questions, please do not hesitate to contact us, here in the forum or on the Motoko GitHub repo. We are grateful if you can try it out and are happy to hear your feedback. Please keep in mind that it is still beta testing - so, please do not yet use it for productive cases once the IC mainnet support for EOP is available.

12 Likes

Great news!

Out of curiosity, I’ve been developing my dApp using dfx 0.23.0.
Up until now, to ensure data persistence after adding elements to my types nested within a hashmap, I’ve had to:

1-Convert my hashmaps into an array of tuples.
2-Utilize the PRE/POST upgrade system functions, for my data to persist.

Question: Since this involves a structural change, does this mean I’ll need to continue using the same method with EOP on dfx 0.24.1-beta.0

Base hash maps will not scale very well As the new allocation algorithms start to fail and hit the instruction limit at high numbers of items. You may want to consider using a more efficient structure, like map or stableheapbtree Depending on your use case.

But as to your initial question, yes, I think this means that we no longer have to worry about dumping stateful objects With references to functions down down to native types that have to be hydrated. :rocket::rocket::rocket:

In fact, this will substantially change the calculus on selecting collection structures in general as Up to now the stable memory requirements have necessitated the use of more functional style libraries. It will be nice to have classic style class objects that just work.

We are going to have to get creative about how objects get upgraded across upgrades As migrating an entire collection upon upgrade, they get a bit instruction heavy.

While, as Luc Says, you won’t hit the instruction limit for the base upgrade, if you have a migration that wants to convert 300 million class objects to a new and improved class type, you’re going to run into some problems trying to do it during your upgrade.

2 Likes

Thank you for asking.

While EOP is focusing on making Motoko upgrades more scalable and efficient, the programming model of stable variables still remains the same, in particular, the restriction on stable types. For avoiding using pre-/post-upgrade hooks, an option could be to use a more non-OO imperative or functional data structure that can directly be used in stable variable, like List, Trie, and several efficient data structures from the base library (e.g. stableheapbtree mentioned by Austin). Another point to consider is that the system-level upgrade (i.e. stable variables) is scalable, however, manual migration by custom pre/post-upgrade code is not necessarily so, as this is just custom programming logic that can hit the instruction limit (or exceed other IC limits, like the memory limit etc.).

On the longer perspective, we may consider to allow also class-based or object-oriented stable types. There are some general ideas on solution approaches, but it is still unclear whether those are entirely feasible and how easily those could be implemented.

1 Like

Ahhh…this is good to know! So we need to still use stable keywords for things we want to make it across the upgrade threshold and if we want class based access we would still need to reconstitute that class in the constructor.

This is actually nice for the time being because it means most of the patterns stay the same. I was actually worried that perhaps our actor class constructor code might not even run after an upgrade and it seems this is a non worry.

It also retains a good bit of the value I’ve invested in setting up some migration and Class+ code to reduce boilerplate.

2 Likes

Just to clarify: if I have a stable TrieMap, EOP alone won’t preserve my data during a canister upgrade, correct? I would still need to follow the standard approach of using pre- and post-upgrade functions, right?

Hi Roland,
Indeed, class instances as well as function references can currently not be persisted, as their types are not stable. Alternatively, data structures like [StableHeapBTree](https://mops.one/stableheapbtreemap ) and RBTree or Trie from the base library are supported for orthogonal persistence.

Good news though: I am currently working on also supporting stable function references and class-based objects for OP.

3 Likes

Wow - Looking forward to learning more about this and seeing what it will look like from a memory footprint perspective.

1 Like

I’m going to wear out this gif :slight_smile: :rocket: :rocket: :rocket: :rocket:

tenor (3)

1 Like

I’ve tested this data structure and confirmed that it can store a record type. Even after a canister update, the data remains accessible.

Are there any specific considerations I should keep in mind if I want to integrate it into my application?

Hi Roland,

stableheapbtreemap is indeed a very fine data-structure. It is performant and also scales large. As far as I can think of, there are no special things to be considered apart the usual upgrade aspects (e.g. that the comparison function needs to stay the same across upgrade to be able to retrieve objects, and that the stored keys, value types cannot mutate, as the data structures uses mutable types internally).

2 Likes