-
Notifications
You must be signed in to change notification settings - Fork 353
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
[IMP] lazy reactive computed value #1499
Conversation
7b47a37
to
bd0e59d
Compare
Hi @sdegueldre and @ged-odoo. What do you think about this? |
ab8c206
to
467fea9
Compare
@sdegueldre Up :p This PR looks very interesting to manage big js project with complex data structure like the POS ! |
**Problem:** Prior to this commit, existing implementations of computed values are eager. An example is the `withComputedProperties` in odoo's web addon, see: [^1]. It has an issue such that it can lead to triggering of the compute function even if the target is still in invalid state -- e.g. removing an item in an array. This can be solved by batching the recomputation with `batched`. But once the compute is batched, the computed value can't be used by other computed values, because it won't recompute until the next tick -- it's not part of the same synchronous context. **Proposed solution:** This commit introduces a implementation of computed value that is pull-based -- it only recomputes when it's needed. The main logic is the following: - A computed value is declared with a single-parameter function wrapped with `computed`. - The compute function has a cache that stores the result of the computation. - The cached value is invalidated when one of the dependencies of the compute function changes. - During invalidation, the cache is only marked as invalid and the recomputation is not made. - A dependent computation such as an effect may indirectly depend in the dependencies of a computed value[^2]. This is an important detail because without this, the 'laziness' of the computed value won't trigger the dependent effect. - This indirect dependency tracking is achieved by using the introduced notion of multi-reactive target where a target can have multiple callbacks linked to it. - During recomputation of the computed value, both the target's reactive and the invalidation callback are subscribed to the dependencies of the computed value. But when the value is cached, only the target's reactive callback is subscribed. With this implementation, we achieve a lazy computed value that plays well with the existing reactive system. [^1]: https://github.com/odoo/odoo/blob/15d7cf12b98bf0223371b8cca91ef6f79c60ee31/addons/web/static/src/core/utils/reactive.js#L58 [^2]: It's possible that in the dependency graph, an effect is affected by invalidation. This effect will be triggered which will potentially ask for the value of the computed value which will trigger the recomputation.
I had a discussion with SEB and he told me about auto-sort compute. He said that it would be better if we could implement automatic in-place sorting without triggering the reactivity. However, this PR can't actually handle that because this PR is just an extension of the existing reactivity, it doesn't actually modify it. So if inplace sort will result to multiple mutations, then that will be the number of times the callback will be called. Even inside a `computed` that's introduced in this PR. Nevertheless, compute that depends on sorted array is still useful, it's just that the sorting should not be done in-place. This commit introduces an example where a computed value that returns a sorted array is only called once (in a batched context) even though multiple mutations are made to the array.
I'm not convinced a priori that we need this but if we do it feels like we could implement this on top of effect relatively simply? function lazyComputed(obj, propName, compute) {
const key = Symbol(propName);
Object.defineProperty(obj, propName, {
get() {
return this[key]();
},
configurable: true,
});
effect(
(obj) => {
const value = [];
obj[key] = () => {
if (!value.length) {
value.push(compute(obj));
}
return value[0];
};
},
[obj]
);
} There are still some shenanigans with a symbol because we don't trap Edit: I've created a version of the todo list app on the playground that auto-sorts the list of tasks: playground link Edit: slightly refined version: function lazyComputed(obj, properties, deps = []) {
for (const [propName, compute] of Object.entries(properties)) {
const key = Symbol(propName);
Object.defineProperty(obj, propName, {
get() {
return this[key]();
},
configurable: true,
});
effect(
(obj, ...deps) => {
const value = [];
obj[key] = () => {
if (!value.length) {
value.push(Reflect.apply(compute, obj, deps));
}
return value[0];
};
},
[obj, ...deps]
);
}
} |
@sdegueldre I really tried to find a way without changing the internal but I couldn't. But man, your solution is awesome. I converted the tests to use your solution and it just works! Knowing this simple solution you provided, we can close this PR. But before I close, you mentioned "There are still some shenanigans with a symbol because we don't trap defineProperty on reactive objects", when you find time, can you tell more about these shenanigans with the symbol? |
As you can see in my implementation, I'm still forced to write and read on a symbol property on the reactive object because at the moment, the reactivity system doesn't consider If that wasn't the case the code could be shortened to just an effect that defines the getter. |
**Problem:** A customer experiences a noticeable delay (1 to 2 sec) when adding product in the cart because of lack of caching computations. It gets worse the more orderlines are added in the cart. **Solution:** This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 This can be used for smart-caching selected getters. In this PR, we made a getter for the `get_all_prices` method and instead of normal getter access, we get the result of the getter using the `get` method introduced in the wrapper class. This means that in one call stack, calling `get('allPrices')` will only run once and it will keep returning the cached value until a dependency in its dependency (calculation) graph changed. With this patch, adding an orderline is now 300ms -- approximate 5 times faster than without caching.
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should _only_ recompute `AB`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that the recomputation is only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, the logic of `X` won't be called. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that the recomputation is only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, the logic of `X` won't be called. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Mutations inside a getter isn't allowed. It might result to infinite loop. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Mutations inside a getter isn't allowed. It might result to infinite loop. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Mutations inside a getter isn't allowed. It might result to infinite loop. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Mutations inside a getter isn't allowed. It might result to infinite loop. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Mutations inside a getter isn't allowed. It might result to infinite loop. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Refrain from making mutations inside a getter. It will likely result to infinite loop if the object being mutated is reactive. But one can still write imperative algorithms on locally defined objects. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Refrain from making mutations inside a getter. It will likely result to infinite loop if the object being mutated is reactive. But one can still write imperative algorithms on locally defined objects. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Refrain from making mutations inside a getter. It will likely result to infinite loop if the object being mutated is reactive. But one can still write imperative algorithms on locally defined objects. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. TASK-ID: 4361605
This commit introduces an implementation of lazy reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Refrain from making mutations inside a getter. It will likely result to infinite loop if the object being mutated is reactive. But one can still write imperative algorithms on locally defined objects. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. TASK-ID: 4361605
This commit introduces an implementation of reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Refrain from making mutations inside a getter. It will likely result to infinite loop if the object being mutated is reactive. But one can still write imperative algorithms on locally defined objects. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. TASK-ID: 4361605
This commit introduces an implementation of reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Refrain from making mutations inside a getter. It will likely result to infinite loop if the object being mutated is reactive. But one can still write imperative algorithms on locally defined objects. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. closes #184315 Task-id: 4361605 Signed-off-by: David Monnom (moda) <[email protected]>
This commit introduces an implementation of reactive computed value that is pull-based -- it only recomputes when it's needed. Check the following PR in odoo/owl for its origin: odoo/owl#1499 It's basically an automatic caching mechanism that taps to the reactivity primitive of odoo/owl. Because it depends on odoo/owl's reactivity, the logic inside a getter is only recomputed when its dependencies changed. Example: Given the following base values `A`, `B`, `C` and `D`, and dependent values `AB`, `BC` and `X`. ```mermaid graph TD A --> AB B --> AB B --> BC C --> BC AB --> X D --> X ``` - Changing `A` should recompute `AB` and `X`. - Changing `B` should recompute `AB`, `BC` and `X`. - Changing `C` should recompute `BC`. - Changing `D` should recompute `X`. It's also pull-based (thus called _lazy_) which means that recomputations are only called when necessary. Imagine a view (component) is only displaying `AB` and `BC` (check above diagram). Changing `D` should invalidate value of `X`, but since the view isn't dependent on `X`, there won't be a rerendering and the logic of `X` won't be called. An obvious benefit of this change is performance but we can consider it as minor. A case where performance change is noticeable is when pos loaded 20k products. In that situation, before this commit, there is a noticeable delay when clicking a product. With this commit, the app feels as if only few products are loaded. Aside from performance, developers also benefits on debugging getters because the logic is only called once per rendering. DX will also be good because we just write basic getters when we want them to be lazy and reactive. Should we want a getter to be called everytime, we define a normal method. IMPORTANT NOTE: Refrain from making mutations inside a getter. It will likely result to infinite loop if the object being mutated is reactive. But one can still write imperative algorithms on locally defined objects. For initial utilization of this feature, we converted few methods into lazy getters. 1. `get productsToDisplay` - This was originally in the `ProductScreen` component. We moved it to `PosStore`. As a result, `productsToDisplay` is now only recomputed when necessary (such as changing the category or changing the search word). Logic inside the getter is not called excessively when `ProductScreen` rerenders, e.g. when adding a new orderline. 2. `getProductsByCategory(category)` is now `get associatedProducts` in the `PosCategory` model. 3. `getUnitDisplayPrice` is now `get unitDisplayPrice` in `PosOrderLine`. 4. `getAllPrices()` and `getAllPrices(1)` calls are now hidden behind the `get allPrices` and `get allUnitPrices` getters, respectively. closes odoo#184315 Task-id: 4361605 Signed-off-by: David Monnom (moda) <[email protected]>
Problem: Prior to this commit, existing implementations of computed values
are eager. An example is the
withComputedProperties
in odoo's web addon, see:1. It has an issue such that it can lead to triggering of the compute
function even if the target is still in invalid state -- e.g. removing an item
in an array. This can be solved by batching the recomputation with
batched
.But once the compute is batched, the computed value can't be used by other
computed values, because it won't recompute until the next tick -- it's not part
of the same synchronous context.
Proposed solution: This commit introduces a implementation of computed value
that is pull-based -- it only recomputes when it's needed. The main logic is the
following:
computed
.function changes.
recomputation is not made.
dependencies of a computed value2. This is an important detail because
without this, the 'laziness' of the computed value won't trigger the dependent
effect.
of multi-reactive target where a target can have multiple callbacks linked
to it.
the invalidation callback are subscribed to the dependencies of the computed
value. But when the value is cached, only the target's reactive callback is
subscribed.
With this implementation, we achieve a lazy computed value that plays well with
the existing reactive system.
Here's an illustration on how the computed values interact with the rendering.
Source
Footnotes
https://github.com/odoo/odoo/blob/15d7cf12b98bf0223371b8cca91ef6f79c60ee31/addons/web/static/src/core/utils/reactive.js#L58 ↩
It's possible that in the dependency graph, an effect is affected by
invalidation. This effect will be triggered which will potentially ask for the
value of the computed value which will trigger the recomputation. ↩