From c2b90552bf9be95b6ae9989551aadef2c5495fa4 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:35:54 -0400 Subject: [PATCH 1/4] Add JSDoc for @cached --- .../@ember/-internals/metal/lib/cached.ts | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/@ember/-internals/metal/lib/cached.ts b/packages/@ember/-internals/metal/lib/cached.ts index aad1fa1572a..ed6911ed01e 100644 --- a/packages/@ember/-internals/metal/lib/cached.ts +++ b/packages/@ember/-internals/metal/lib/cached.ts @@ -4,7 +4,39 @@ import { DEBUG } from '@glimmer/env'; import { createCache, getValue } from '@glimmer/validator'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any +/** + * @decorator + * + * Memoizes the result of a getter based on autotracking. + * + * The `@cached` decorator can be used on native getters to memoize their return + * values based on the tracked state they consume while being calculated. + * + * By default a getter is always re-computed every time it is accessed. On + * average this is faster than caching every getter result by default. + * + * 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 helps developers + * optimize their apps when relevant, without adding extra overhead unless + * necessary. + * + * @example + * + * ```ts + * import { tracked, cached } from '@glimmer/tracking'; + * + * class Person { + * @tracked firstName = 'Jen'; + * @tracked lastName = 'Weber'; + * + * @cached + * get fullName() { + * return `${this.firstName} ${this.lastName}`; + * } + * } + * ``` + */ export const cached: PropertyDecorator = (...args: any[]) => { const [target, key, descriptor] = args; From 622ee60daaa103ef02f03e281fcd29a030275dad Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:43:20 -0400 Subject: [PATCH 2/4] match guides docs --- .../@ember/-internals/metal/lib/cached.ts | 119 +++++++++++++----- 1 file changed, 90 insertions(+), 29 deletions(-) diff --git a/packages/@ember/-internals/metal/lib/cached.ts b/packages/@ember/-internals/metal/lib/cached.ts index ed6911ed01e..6d57bf7c804 100644 --- a/packages/@ember/-internals/metal/lib/cached.ts +++ b/packages/@ember/-internals/metal/lib/cached.ts @@ -7,35 +7,96 @@ import { createCache, getValue } from '@glimmer/validator'; /** * @decorator * - * Memoizes the result of a getter based on autotracking. - * - * The `@cached` decorator can be used on native getters to memoize their return - * values based on the tracked state they consume while being calculated. - * - * By default a getter is always re-computed every time it is accessed. On - * average this is faster than caching every getter result by default. - * - * 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 helps developers - * optimize their apps when relevant, without adding extra overhead unless - * necessary. - * - * @example - * - * ```ts - * import { tracked, cached } from '@glimmer/tracking'; - * - * class Person { - * @tracked firstName = 'Jen'; - * @tracked lastName = 'Weber'; - * - * @cached - * get fullName() { - * return `${this.firstName} ${this.lastName}`; - * } - * } - * ``` + Gives the getter a caching behavior. The return value of the getter + will be cached until any of the properties it is entangled with + are invalidated. This is useful when a getter is expensive and + used very often. + + For instance, in this `GuestList` class, we have the `sortedGuests` + getter that sorts the guests alphabetically: + + ```javascript + import { tracked } from '@glimmer/tracking'; + + class GuestList { + @tracked guests = ['Zoey', 'Tomster']; + + get sortedGuests() { + return this.guests.slice().sort() + } + } + ``` + + Every time `sortedGuests` is accessed, a new array will be created and sorted, + because JavaScript getters do not cache by default. When the guest list + is small, like the one in the example, this is not a problem. However, if + the guest list were to grow very large, it would mean that we would be doing + a large amount of work each time we accessed `sortedGuests`. With `@cached`, + we can cache the value instead: + + ```javascript + import { tracked, cached } from '@glimmer/tracking'; + + class GuestList { + @tracked guests = ['Zoey', 'Tomster']; + + @cached + get sortedGuests() { + return this.guests.slice().sort() + } + } + ``` + + Now the `sortedGuests` getter will be cached based on autotracking. + It will only rerun and create a new sorted array when the guests tracked + property is updated. + + + ### Tradeoffs + + Overuse is discouraged. + + In general, you should avoid using `@cached` unless you have confirmed that + the getter you are decorating is computationally expensive, since `@cached` + adds a small amount of overhead to the getter. + While the individual costs are small, a systematic use of the `@cached` + decorator can add up to a large impact overall in your app. + Many getters and tracked properties are only accessed once during rendering, + and then never rerendered, so adding `@cached` when unnecessary can + negatively impact performance. + + Also, `@cached` may rerun even if the values themselves have not changed, + since tracked properties will always invalidate. + For example updating an integer value from `5` to an other `5` will trigger + a rerun of the cached properties building from this integer. + + Avoiding a cache invalidation in this case is not something that can + be achieved on the `@cached` decorator itself, but rather when updating + the underlying tracked values, by applying some diff checking mechanisms: + + ```javascript + if (nextValue !== this.trackedProp) { + this.trackedProp = nextValue; + } + ``` + + Here equal values won't update the property, therefore not triggering + the subsequent cache invalidations of the `@cached` properties who were + using this `trackedProp`. + + As a reminder, do not use this piece of code inside a tracked getter, + as the dependency chain could lead to an infinite loop. Mutating an adjacent + property from a getter is not a good practice anyway, even with a caching + mechanism reducing reruns. + + The cost of these edge-guards adds up to the trade-off calculation of using + a caching strategy, hence requiring a very sensitive and moderate approach + regarding performance. + + @method cached + @static + @for @glimmer/tracking + @public */ export const cached: PropertyDecorator = (...args: any[]) => { const [target, key, descriptor] = args; From 6f79768ce5fd31b906f2ce93c08f6f93ecd387a3 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:47:07 -0400 Subject: [PATCH 3/4] Update packages/@ember/-internals/metal/lib/cached.ts Co-authored-by: Matthew Beale --- packages/@ember/-internals/metal/lib/cached.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/@ember/-internals/metal/lib/cached.ts b/packages/@ember/-internals/metal/lib/cached.ts index 6d57bf7c804..c378021968b 100644 --- a/packages/@ember/-internals/metal/lib/cached.ts +++ b/packages/@ember/-internals/metal/lib/cached.ts @@ -89,10 +89,6 @@ import { createCache, getValue } from '@glimmer/validator'; property from a getter is not a good practice anyway, even with a caching mechanism reducing reruns. - The cost of these edge-guards adds up to the trade-off calculation of using - a caching strategy, hence requiring a very sensitive and moderate approach - regarding performance. - @method cached @static @for @glimmer/tracking From e0bb76a1e7c29c37e9eac9fc0ac26fcd364460d1 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:52:00 -0400 Subject: [PATCH 4/4] Update cached.ts --- packages/@ember/-internals/metal/lib/cached.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/@ember/-internals/metal/lib/cached.ts b/packages/@ember/-internals/metal/lib/cached.ts index c378021968b..8e6bdfb4d4e 100644 --- a/packages/@ember/-internals/metal/lib/cached.ts +++ b/packages/@ember/-internals/metal/lib/cached.ts @@ -84,10 +84,9 @@ import { createCache, getValue } from '@glimmer/validator'; the subsequent cache invalidations of the `@cached` properties who were using this `trackedProp`. - As a reminder, do not use this piece of code inside a tracked getter, - as the dependency chain could lead to an infinite loop. Mutating an adjacent - property from a getter is not a good practice anyway, even with a caching - mechanism reducing reruns. + Remember that setting tracked data should only be done during initialization, + or as the result of a user action. Setting tracked data during render + (such as in a getter), is not supported. @method cached @static