From 0f3c48ef430605f4d185b77bd043529dac7f9dff Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Sun, 22 Dec 2019 19:19:37 -0800 Subject: [PATCH] Adds @memo RFC --- text/0000-memo-decorator.md | 141 ++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 text/0000-memo-decorator.md diff --git a/text/0000-memo-decorator.md b/text/0000-memo-decorator.md new file mode 100644 index 0000000000..9fa0840898 --- /dev/null +++ b/text/0000-memo-decorator.md @@ -0,0 +1,141 @@ +- Start Date: 2019-12-22 +- Relevant Team(s): Ember.js +- RFC PR: (after opening the RFC PR, update this with a link to it and update the file name) +- Tracking: (leave this empty) + +# @memo + +## Summary + +Add a `@memo` decorator for memoizing the result of a getter based on +autotracking. In the following example, `fullName` would only recalculate if +`firstName` or `lastName` is updated. + +```js +class Person { + @tracked firstName = 'Jen'; + @tracked lastName = 'Weber'; + + @memo + get fullName() { + return `${this.firstName} ${this.lastName}`; + } +} +``` + +## Motivation + +One of the major differences between computed properties and tracked properties +with autotracking in Octane is that native, autotracked getters do not +automatically cache their values, where computed properties were cached by +default. This was an intentional design choice, as the memoization logic for +computed properties was actually more costly, on average, than rerunning the +getter in the first place. This was especially true given that computed +properties would usually only ever be calculated and used once or twice per +render before being updated. + +However, there are absolutely cases where getters _are_ expensive, and their +values are used repeatedly, so memoization would be very helpful. Strategic, +opt-in memoization is a useful tool that would help Ember developers optimize +their apps when relevant, without adding extra overhead unless necessary. + +## Detailed design + +The `@memo` decorator will be exported from `@glimmer/tracking`, alongside +`@tracked`. It can be used on native getters to memoize their return values +based on the tracked state they consume while being calculated. + +```js +import { tracked, memo } from '@glimmer/tracking'; + +class Person { + @tracked firstName = 'Jen'; + @tracked lastName = 'Weber'; + + @memo + get fullName() { + return `${this.firstName} ${this.lastName}`; + } +} +``` + +In this example, the `fullName` getter will be memoized whenever it is called, +and will only be recalculated the next time the `firstName` or `lastName` +properties are set. This would apply to any autotracking tags consumed while +calculating the getter, so changes to `EmberArray`s and other tracked +primitives, for instance, would also cause invalidations. + +If used on a non-getter, `@memo` will throw an error in `DEBUG` modes. +Properties can also include a setter, but it won't affect the memoization of the +getter (except by potentially setting the state that was tracked in the first +place). + +### Invalidation + +`@memo` will propagate invalidations whenever any of the properties it is +entangled with are invalidated, causing any downstream state that has consumed +to be invalidated at the same time. + +This is necessary in order to have the memoized value be pulled on again in +general. There is no way to, for instance, only propagate changes if the +memoized value has changed, since we must calculate the memoized value first to +know what it's new value is, and we must propagate the change in order to ensure +that it will be recalculated. + +### Cycles + +Cycles will not be allowed with `@memo`. The cache will only be activated +_after_ the getter has fully calculated, so any cycles will cause infinite +recursion (and eventually, stack overflow), just like un-memoized getters. If a +cycle is detected in `DEBUG` mode, it will throw an error. + +## How we teach this + +`@memo` is not an essential part of the reactivity model, so it shouldn't be +covered during the main component/reactivity guide flow. Instead, it should be +covered in the intermediate/in-depth guides, and in any performance related +guides. + +### API Docs + +TODO + +## Drawbacks + +- Adds extra complexity when programming (whether or not a value should be + memoized is now a decision that has to be made). In general, we should make + sure this is not an issue by recommending that memoization idiomatically _not_ + be used _unless_ it is absolutely necessary. + +- Adds extra overhead for each memoized getter. This again should be addressed + by teaching that it should be avoided when possible. The cost tradeoff should + be noted in documentation in particular to emphasize this, and discourage + overuse. + +- `@memo` may rerun even if the values themselves have not changed, since + tracked properties will always invalidate even if their underlying value did + not change. Unfortunately, this is not really something that `@memo` can + circumvent, since there's no way to tell if a value has actually changed, or + to know which values are being accessed when the memoized value is accessed + until the getter is run. + + Instead, we should be sure that the rules of property invalidation are clear, + and in performance sensitive situations we recommend diff checking when + assigning the property: + + ```js + if (newValue !== this.trackedProp) { + this.trackedProp = newValue; + } + ``` + +## Alternatives + +- `@cached` or `@memoized` could be alternative names. + +- `@memo` could receive arguments of the keys to memoize based on. This would + bring us back to the ergonomics of computed properties, however, and would not + be ideal. It also would bring no actual benefits, except being able to exclude + certain values from recalculation. + +