Skip to content

Commit

Permalink
[PERF] point_of_sale: lazy reactive getter
Browse files Browse the repository at this point in the history
**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.
  • Loading branch information
caburj committed Jul 26, 2024
1 parent 7193551 commit ddc6817
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 11 deletions.
82 changes: 82 additions & 0 deletions addons/point_of_sale/static/src/app/models/lazy_computed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/** @odoo-module */

import { effect } from "@web/core/utils/reactive";

function lazyComputed(obj, propName, compute) {
const key = Symbol(propName);
Object.defineProperty(obj, propName, {
get() {
return this[key]();
},
configurable: true,
});

effect(
function recompute(obj) {
const value = [];
obj[key] = () => {
if (!value.length) {
value.push(compute(obj));
}
return value[0];
};
},
[obj]
);
}

function getAllGetters(proto) {
const getterNames = new Set();
const getters = new Set();
while (proto !== null) {
const descriptors = Object.getOwnPropertyDescriptors(proto);
for (const [name, descriptor] of Object.entries(descriptors)) {
if (descriptor.get && !getterNames.has(name)) {
getterNames.add(name);
getters.add([name, descriptor.get]);
}
}
proto = Object.getPrototypeOf(proto);
}
return getters;
}

export function createModelWithLazyGetters(Class) {
const getters = new Map();
let gettersRegistered = false;
/**
* Getters are registered to the auto-caching mechanism of the lazyComputed.
* To use the auto-caching, a getter must be accessed using the `get` method.
*/
function registerGetters() {
for (const [name, func] of getAllGetters(WithLazyGetters.prototype)) {
if (name.startsWith("__") && name.endsWith("__")) {
continue;
}
const lazyName = `__lazy_${name}`;
getters.set(name, [lazyName, (obj) => func.call(obj)]);
}
gettersRegistered = true;
}
class WithLazyGetters extends Class {
constructor(...args) {
// before creating the first instance, we register the getters
if (!gettersRegistered) {
registerGetters();
}
super(...args);
for (const [lazyName, func] of getters.values()) {
lazyComputed(this, lazyName, func);
}
}
get(getterName) {
const [lazyName] = getters.get(getterName);
if (lazyName) {
return this[lazyName];
} else {
throw new Error(`Getter ${getterName} is not defined.`);
}
}
}
return WithLazyGetters;
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class ReceiptScreen extends Component {
const tipLine = order
.get_orderlines()
.find((line) => tip_product_id && line.product.id === tip_product_id);
const tipAmount = tipLine ? tipLine.get_all_prices().priceWithTax : 0;
const tipAmount = tipLine ? tipLine.get("allPrices").priceWithTax : 0;
const orderAmountStr = this.env.utils.formatCurrency(orderTotalAmount - tipAmount);
if (!tipAmount) {
return orderAmountStr;
Expand Down
27 changes: 18 additions & 9 deletions addons/point_of_sale/static/src/app/store/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { renderToElement } from "@web/core/utils/render";
import { ProductCustomAttribute } from "./models/product_custom_attribute";
import { omit } from "@web/core/utils/objects";
import { ask } from "@point_of_sale/app/store/make_awaitable_dialog";
import { createModelWithLazyGetters } from "../models/lazy_computed";

const { DateTime } = luxon;

Expand Down Expand Up @@ -66,7 +67,7 @@ var orderline_id = 1;
// An orderline represent one element of the content of a customer's shopping cart.
// An orderline contains a product, its quantity, its price, discount. etc.
// An Order contains zero or more Orderlines.
export class Orderline extends PosModel {
class Orderline_Original extends PosModel {
setup(_defaultObj, options) {
super.setup(...arguments);
this.pos = options.pos;
Expand Down Expand Up @@ -581,9 +582,9 @@ export class Orderline extends PosModel {
}
get_unit_display_price() {
if (this.pos.config.iface_tax_included === "total") {
return this.get_all_prices(1).priceWithTax;
return this.get("allPricesUnit").priceWithTax;
} else {
return this.get_all_prices(1).priceWithoutTax;
return this.get("allPricesUnit").priceWithoutTax;
}
}
/**
Expand All @@ -600,9 +601,9 @@ export class Orderline extends PosModel {
}
getUnitDisplayPriceBeforeDiscount() {
if (this.pos.config.iface_tax_included === "total") {
return this.get_all_prices(1).priceWithTaxBeforeDiscount;
return this.get("allPricesUnit").priceWithTaxBeforeDiscount;
} else {
return this.get_all_prices(1).priceWithoutTaxBeforeDiscount;
return this.get("allPricesUnit").priceWithoutTaxBeforeDiscount;
}
}
get_base_price() {
Expand Down Expand Up @@ -639,16 +640,16 @@ export class Orderline extends PosModel {
}
}
get_price_without_tax() {
return this.get_all_prices().priceWithoutTax;
return this.get("allPrices").priceWithoutTax;
}
get_price_with_tax() {
return this.get_all_prices().priceWithTax;
return this.get("allPrices").priceWithTax;
}
get_tax() {
return this.get_all_prices().tax;
return this.get("allPrices").tax;
}
get_tax_details() {
return this.get_all_prices().taxDetails;
return this.get("allPrices").taxDetails;
}
get_taxes() {
const taxes_ids =
Expand Down Expand Up @@ -724,6 +725,12 @@ export class Orderline extends PosModel {
taxValuesList: taxesData.taxes_data,
};
}
get allPrices() {
return this.get_all_prices();
}
get allPricesUnit() {
return this.get_all_prices(1);
}
display_discount_policy() {
return this.order.pricelist ? this.order.pricelist.discount_policy : "with_discount";
}
Expand Down Expand Up @@ -828,6 +835,8 @@ export class Orderline extends PosModel {
}
}

export const Orderline = createModelWithLazyGetters(Orderline_Original);

export class Packlotline extends PosModel {
setup(_defaultObj, options) {
super.setup(...arguments);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class SplitBillScreen extends Component {
setup() {
this.pos = usePos();
this.splitlines = useState(this._initSplitLines(this.pos.get_order()));
this.newOrderLines = {};
this.newOrderLines = useState({});
this.newOrder = undefined;
this._isFinal = false;
this.newOrder = new Order(
Expand Down

0 comments on commit ddc6817

Please sign in to comment.