Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] stores: bye bye reactivity 馃憢 #4104

Closed
wants to merge 1 commit into from

Conversation

LucasLefevre
Copy link
Collaborator

@LucasLefevre LucasLefevre commented Apr 19, 2024

Description:

Reactivity is very very (very) slow, with potential overhead exceeding 90%.

With a reactive store, every this.something call traverses the reactive
proxy, incurring significant cost.

This commit completely revisit the way store updates trigger app rendering.
Previously, this process relied entirely on Owl's reactivity system.

Instead, it introduces a new approach where store updates are triggered by
explicitly declared public methods (mutators)
Since stores follow the CQS principle all public methods actually
mutate state in a store, they cannot have any other effect since they
can't return anything.

The new approach also relies on proxies to track mutators calls, but
the proxy is only on the surface. A mutator is usually only called once
in the component. A properties is also usually accessed only once for
rendering. The internal working of the store is not affected and can
run at full speed without any overhead.

Note the loss of fined-grained rendering. However, as observed, bypassing
reactivity might result in faster overall app rendering compared to
relying on reactivity for fine-grained rendering, even if it means rendering
the entire app rather than a few components.

Reactivity had another unexpected error-prone issue: since everything
was proxied, we could never compare objects references without using
toRaw to retrieve the underlying un-proxied object. It was very
error prone and added noise in the code.

On the large number data set, open the find & replace panel and search for "1"
It currently takes 1080ms (search + rendering)
With this commit, that time is reduced to just 111ms, nearly a tenfold
improvement!

Task: : 3829125

review checklist

  • feature is organized in plugin, or UI components
  • support of duplicate sheet (deep copy)
  • in model/core: ranges are Range object, and can be adapted (adaptRanges)
  • in model/UI: ranges are strings (to show the user)
  • undo-able commands (uses this.history.update)
  • multiuser-able commands (has inverse commands and transformations where needed)
  • new/updated/removed commands are documented
  • exportable in excel
  • translations (_t("qmsdf %s", abc))
  • unit tested
  • clean commented code
  • track breaking changes
  • doc is rebuild (npm run doc)
  • status is correct in Odoo

@robodoo
Copy link
Collaborator

robodoo commented Apr 19, 2024

@LucasLefevre LucasLefevre force-pushed the saas-17.2-stores-bye-reactivity-lul branch from bbaf339 to 472403b Compare April 19, 2024 18:43
@LucasLefevre LucasLefevre force-pushed the saas-17.2-stores-bye-reactivity-lul branch 7 times, most recently from f8b0ca3 to f4b34aa Compare May 14, 2024 07:19
Comment on lines 42 to 43
// @ts-ignore
rendererManager.drawLayer(renderingContext, layer);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drawLayer is a method used during rendering. It's not a mutator.

src/store_engine/dependency_container.ts Show resolved Hide resolved
@@ -15,8 +15,8 @@ export function useStoreProvider() {
__spreadsheet_stores__: container,
getStore: <T extends StoreConstructor>(Store: T) => {
const store = container.get(Store);
const mutators: readonly string[] = store.mutators;
return proxifyToRender(env, store, mutators);
// TODO check this, it shouldn't be/use a hook here
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because useStoreRenderProxy uses useCompent which is only available in the setup of components. (right click on a cell, Insert link => boom)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah right :'(

@@ -57,20 +56,14 @@ import { RendererStore } from "./renderer_store";
export const CELL_BACKGROUND_GRIDLINE_STROKE_STYLE = "#111";

export class GridRenderer {
mutators = [] as const;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it mandatory to declare for stores? the typing seems to miss this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a store makes sense without mutators ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a store makes sense without mutators ?

currently yes because other stores need to register themselves to this store

Copy link
Collaborator

@pro-odoo pro-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice 馃コ Bye bye toRaw and thanks for nothing

@@ -84,9 +93,9 @@ export class ComposerStore extends SpreadsheetStore {

constructor(get: Get) {
super(get);
this.highlightStore.register(toRaw(this));
this.highlightStore.register(this);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃コ

@@ -57,20 +56,14 @@ import { RendererStore } from "./renderer_store";
export const CELL_BACKGROUND_GRIDLINE_STROKE_STYLE = "#111";

export class GridRenderer {
mutators = [] as const;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a store makes sense without mutators ?

@LucasLefevre LucasLefevre force-pushed the saas-17.2-stores-bye-reactivity-lul branch 3 times, most recently from 082434b to 80dac25 Compare May 17, 2024 12:01
@LucasLefevre LucasLefevre changed the title [POC] stores: bye bye reactivity 馃憢 [FIX] stores: bye bye reactivity 馃憢 May 17, 2024
@LucasLefevre LucasLefevre force-pushed the saas-17.2-stores-bye-reactivity-lul branch 3 times, most recently from 5a8d43e to 88bbda7 Compare May 17, 2024 12:25
@rrahir
Copy link
Collaborator

rrahir commented May 21, 2024

Could be the occasion to get rid of "RENDER_CANVAS" command then since we have a new bus to work with?

diff --git a/src/components/helpers/highlight_hook.ts b/src/components/helpers/highlight_hook.ts
index 7c0edb7e0..4fb2116f5 100644
--- a/src/components/helpers/highlight_hook.ts
+++ b/src/components/helpers/highlight_hook.ts
@@ -1,12 +1,12 @@
-import { onMounted, useEffect, useEnv } from "@odoo/owl";
-import { useLocalStore } from "../../store_engine";
+import { onMounted, useEffect } from "@odoo/owl";
+import { useLocalStore, useStoreProvider } from "../../store_engine";
 import { HighlightProvider, HighlightStore } from "../../stores/highlight_store";
-import { Ref, SpreadsheetChildEnv } from "../../types";
+import { Ref } from "../../types";
 import { useHoveredElement } from "./listener_hook";
 
 export function useHighlightsOnHover(ref: Ref<HTMLElement>, highlightProvider: HighlightProvider) {
   const hoverState = useHoveredElement(ref);
-  const env = useEnv() as SpreadsheetChildEnv;
+  const stores = useStoreProvider();
 
   useHighlights({
     get highlights() {
@@ -15,7 +15,7 @@ export function useHighlightsOnHover(ref: Ref<HTMLElement>, highlightProvider: H
   });
   useEffect(
     () => {
-      env.model.dispatch("RENDER_CANVAS");
+      stores.trigger("store-updated");
     },
     () => [hoverState.hovered]
   );
``` @LucasLefevre WDYT?

@LucasLefevre
Copy link
Collaborator Author

@rrahir true. I pushed a commit :)

Copy link
Collaborator

@rrahir rrahir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok for me :)

Copy link
Collaborator

@pro-odoo pro-odoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃憤

Comment on lines +316 to +336
/**
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
*
* @param callback the callback to batch
* @returns a batched version of the original callback
*
* Copied from odoo/owl repo.
*/
export function batched(callback: () => void): () => void {
let scheduled = false;
return async (...args) => {
if (!scheduled) {
scheduled = true;
await Promise.resolve();
scheduled = false;
callback(...args);
}
};
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't batched be linked to the nextanimationframe instead of the microtick ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a commit but it's 馃あ just because we don't have a direct access to owl's Scheduler (same trick used in the test).
I'll go and talk to the framework-js team tomorrow to check if they can export Scheduler from owl (or an updated version of their batched using Scheduler.requestAnimationFrame)

@VincentSchippefilt
Copy link
Collaborator

I think it is a fantastic idea. good job !

Comment on lines 323 to 318
* Creates a batched version of a callback so that all calls to it in the same
* microtick will only call the original callback once.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THIS IS A LIE :)

@VincentSchippefilt
Copy link
Collaborator

what would be less error prone:

class MyStore extends Store {
 constructor() {...}
 static mutators = ["myMytator","myOtherMutator"]
 myMutator(value) { ... }
 myOtherMutator(value) { ... }
 myGetter() { return value; }
}

or

class MyStore extends Store {
 constructor() {...}
 mutators : {
    myMutator = function (value) { ... },
    myOtherMutator = function (value) { ... }
 }
 myGetter() { return value; } // if I write this.mutators.myMutator here, it's obviously a bug
}

Reactivity is very very (very) slow, with potential overhead exceeding 90%.

This commit completely revisit the way store updates trigger app rendering.
Previously, this process relied entirely on Owl's reactivity system.

With a reactive store, every `this.something` call traverses the reactive
proxy, incurring significant cost, regardless of whether `something` was
a method, a property, called publicly or privately.

Instead, it introduces a new approach where store updates are triggered by
explicitly declared public methods (mutators)
Since stores follow the Command-Query Separation (CQS) principle, all
public methods strictly mutate state in a store, they cannot have any
other effect since they cannot return anything.

The new approach also relies on proxies to track mutators calls, but
the proxy is only on the surface. A mutator is usually only called once
in the component. A property is also usually accessed only once for
rendering. The internal working of the store is not affected and can
run at full speed without any overhead.

Note the loss of fined-grained rendering. However, as observed, bypassing
reactivity might result in faster overall app rendering compared to
relying on reactivity for fine-grained rendering, even if it means rendering
the entire app rather than a few components.

Reactivity had another unexpected error-prone issue: since everything
was proxied, we could never compare objects references without using
`toRaw` to retrieve the underlying un-proxied object. It was very
error prone and added noise in the code.

On the large number data set, open the find & replace panel and search for "1"
It currently takes 1080ms (search + rendering)
With this commit, that time is reduced to just 111ms, nearly a tenfold
improvement!

Task: 3829125
@LucasLefevre LucasLefevre force-pushed the saas-17.2-stores-bye-reactivity-lul branch from edd3c0d to 5f4bcfc Compare May 22, 2024 08:35
@rrahir
Copy link
Collaborator

rrahir commented May 22, 2024

@robodoo r+

robodoo pushed a commit that referenced this pull request May 22, 2024
Reactivity is very very (very) slow, with potential overhead exceeding 90%.

This commit completely revisit the way store updates trigger app rendering.
Previously, this process relied entirely on Owl's reactivity system.

With a reactive store, every `this.something` call traverses the reactive
proxy, incurring significant cost, regardless of whether `something` was
a method, a property, called publicly or privately.

Instead, it introduces a new approach where store updates are triggered by
explicitly declared public methods (mutators)
Since stores follow the Command-Query Separation (CQS) principle, all
public methods strictly mutate state in a store, they cannot have any
other effect since they cannot return anything.

The new approach also relies on proxies to track mutators calls, but
the proxy is only on the surface. A mutator is usually only called once
in the component. A property is also usually accessed only once for
rendering. The internal working of the store is not affected and can
run at full speed without any overhead.

Note the loss of fined-grained rendering. However, as observed, bypassing
reactivity might result in faster overall app rendering compared to
relying on reactivity for fine-grained rendering, even if it means rendering
the entire app rather than a few components.

Reactivity had another unexpected error-prone issue: since everything
was proxied, we could never compare objects references without using
`toRaw` to retrieve the underlying un-proxied object. It was very
error prone and added noise in the code.

On the large number data set, open the find & replace panel and search for "1"
It currently takes 1080ms (search + rendering)
With this commit, that time is reduced to just 111ms, nearly a tenfold
improvement!

closes #4104

Task: 3829125
Signed-off-by: R茅mi Rahir (rar) <rar@odoo.com>
@robodoo robodoo closed this May 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants