PromucFlow_constructor/app/client/src/workers/Evaluation/evalTreeWithChanges.test.ts
Vemparala Surya Vamsi 2b9299e2d3
chore: bypass immer for first evaluation, fixed cloneDeep issue and using mutative instead of immer (#38993)
## Description
- Using mutative instead of immer, this has reduced the main thread
scripting by about 1 second.
- Removed a cloneDeep during table onMount which saves about 70ms in
main thread scripting.
- Bypassed mutative when applying the first tree to reduce the overhead
associated to mutative.

Fixes #`Issue Number`  
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/13164792224>
> Commit: 4cb821723d10198c9db70312a9604df5aa5f80c1
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13164792224&attempt=2"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 06 Feb 2025 04:21:41 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Dependency**
  - Integrated a new library to enhance overall state management.

- **Refactor**
- Updated state update mechanisms across interactive components and data
flows.
  - Improved table widget processing for more consistent behavior.

- **Chore**
  - Removed legacy development-only configuration settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-06 11:20:08 +05:30

570 lines
17 KiB
TypeScript

import type { WidgetEntityConfig } from "ee/entities/DataTree/types";
import { DataTreeDiffEvent } from "ee/workers/Evaluation/evaluationUtils";
import { RenderModes } from "constants/WidgetConstants";
import { ENTITY_TYPE } from "ee/entities/DataTree/types";
import type { ConfigTree } from "entities/DataTree/dataTreeTypes";
import { generateDataTreeWidget } from "entities/DataTree/dataTreeWidget";
import { create } from "mutative";
import type { WidgetEntity } from "plugins/Linting/lib/entity/WidgetEntity";
import type { UpdateDataTreeMessageData } from "sagas/EvalWorkerActionSagas";
import DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import * as evalTreeWithChanges from "./evalTreeWithChanges";
import { APP_MODE } from "entities/App";
export const BASE_WIDGET = {
widgetId: "randomID",
widgetName: "randomWidgetName",
bottomRow: 0,
isLoading: false,
leftColumn: 0,
parentColumnSpace: 0,
parentRowSpace: 0,
renderMode: RenderModes.CANVAS,
rightColumn: 0,
topRow: 0,
type: "SKELETON_WIDGET",
parentId: "0",
version: 1,
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
meta: {},
} as unknown as WidgetEntity;
export const BASE_WIDGET_CONFIG = {
logBlackList: {},
widgetId: "randomID",
type: "SKELETON_WIDGET",
ENTITY_TYPE: ENTITY_TYPE.WIDGET,
} as unknown as WidgetEntityConfig;
const WIDGET_CONFIG_MAP = {
TEXT_WIDGET: {
defaultProperties: {},
derivedProperties: {
value: "{{ this.text }}",
},
metaProperties: {},
},
};
const configTree: ConfigTree = {
Text1: generateDataTreeWidget(
{
...BASE_WIDGET_CONFIG,
...BASE_WIDGET,
widgetName: "Text1",
text: "Label",
type: "TEXT_WIDGET",
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
{},
new Set(),
).configEntity,
Text2: generateDataTreeWidget(
{
...BASE_WIDGET_CONFIG,
...BASE_WIDGET,
widgetName: "Text2",
text: "{{Text1.text}}",
dynamicBindingPathList: [{ key: "text" }],
type: "TEXT_WIDGET",
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
{},
new Set(),
).configEntity,
};
const unEvalTree = {
Text1: generateDataTreeWidget(
{
...BASE_WIDGET_CONFIG,
...BASE_WIDGET,
widgetName: "Text1",
text: "Label",
type: "TEXT_WIDGET",
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
{},
new Set(),
).unEvalEntity,
Text2: generateDataTreeWidget(
{
...BASE_WIDGET_CONFIG,
...BASE_WIDGET,
widgetName: "Text2",
text: "{{Text1.text}}",
dynamicBindingPathList: [{ key: "text" }],
type: "TEXT_WIDGET",
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
{},
new Set(),
).unEvalEntity,
};
describe("evaluateAndPushResponse", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let pushResponseToMainThreadMock: any;
beforeAll(() => {
pushResponseToMainThreadMock = jest
.spyOn(evalTreeWithChanges, "pushResponseToMainThread")
.mockImplementation(() => {}); // spy on foo
});
beforeAll(() => {
jest.clearAllMocks();
});
test("should call pushResponseToMainThread when we evaluate and push updates", () => {
evalTreeWithChanges.evaluateAndPushResponse(
undefined,
{
unEvalUpdates: [],
evalOrder: [],
jsUpdates: {},
},
[],
[],
);
// check if push response has been called
expect(pushResponseToMainThreadMock).toHaveBeenCalled();
});
});
describe("getAffectedNodesInTheDataTree", () => {
test("should merge paths from unEvalUpdates and evalOrder", () => {
const result = evalTreeWithChanges.getAffectedNodesInTheDataTree(
[
{
event: DataTreeDiffEvent.NOOP,
payload: {
propertyPath: "Text2.text",
value: "",
},
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
],
["Text1.text"],
);
expect(result).toEqual(["Text2.text", "Text1.text"]);
});
test("should extract unique paths from unEvalUpdates and evalOrder", () => {
const result = evalTreeWithChanges.getAffectedNodesInTheDataTree(
[
{
event: DataTreeDiffEvent.NOOP,
payload: {
propertyPath: "Text1.text",
value: "",
},
},
],
["Text1.text"],
);
expect(result).toEqual(["Text1.text"]);
});
});
describe("evaluateAndGenerateResponse", () => {
let evaluator: DataTreeEvaluator;
const UPDATED_LABEL = "updated Label";
const getParsedUpdatesFromWebWorkerResp = (
webworkerResponse: UpdateDataTreeMessageData,
) => {
const updates = JSON.parse(webworkerResponse.workerResponse.updates);
//scrub out all __evaluation__ patches
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return updates.filter((p: any) => !p.rhs.__evaluation__);
};
beforeEach(async () => {
evaluator = new DataTreeEvaluator(WIDGET_CONFIG_MAP);
await evaluator.setupFirstTree(
unEvalTree,
configTree,
{},
{
appId: "appId",
pageId: "pageId",
timestamp: "timestamp",
appMode: APP_MODE.PUBLISHED,
instanceId: "instanceId",
},
);
evaluator.evalAndValidateFirstTree();
});
test("inital evaluation successful should be successful", () => {
expect(evaluator.evalTree).toHaveProperty("Text2.text", "Label");
});
test("should respond with default values when dataTreeEvaluator is not provided", () => {
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
undefined,
{
unEvalUpdates: [],
evalOrder: [],
jsUpdates: {},
},
[],
[],
);
const parsedUpdates = getParsedUpdatesFromWebWorkerResp(webworkerResponse);
expect(parsedUpdates).toEqual([]);
expect(webworkerResponse).toEqual({
workerResponse: {
dependencies: {},
errors: [],
evalMetaUpdates: [],
evaluationOrder: [],
isCreateFirstTree: false,
isNewWidgetAdded: false,
jsUpdates: {},
jsVarsCreatedEvent: [],
logs: [],
removedPaths: [],
staleMetaIds: [],
unEvalUpdates: [],
undefinedEvalValuesMap: {},
updates: "[]",
},
});
});
test("should generate no updates when the updateTreeResponse is empty", () => {
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
{
unEvalUpdates: [],
evalOrder: [],
jsUpdates: {},
},
[],
[],
);
const parsedUpdates = getParsedUpdatesFromWebWorkerResp(webworkerResponse);
expect(parsedUpdates).toEqual([]);
});
describe("updates", () => {
test("should generate updates based on the unEvalUpdates", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
draft.Text1.text = UPDATED_LABEL;
draft.Text1.label = UPDATED_LABEL;
});
const updateTreeResponse = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
// ignore label Text1.label uneval update and just include Text1.text uneval update
updateTreeResponse.unEvalUpdates = [
{
event: DataTreeDiffEvent.NOOP,
payload: {
propertyPath: "Text1.text",
value: "",
},
},
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any;
// the eval tree should have the uneval update but the diff should not be generated because the unEvalUpdates has been altered
expect(evaluator.evalTree).toHaveProperty("Text1.text", UPDATED_LABEL);
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
updateTreeResponse,
[],
[],
);
expect(webworkerResponse.workerResponse.dependencies).toEqual({
"Text1.text": ["Text2.text", "Text1"],
"Text2.text": ["Text2"],
});
const parsedUpdates =
getParsedUpdatesFromWebWorkerResp(webworkerResponse);
// Text1.label update should be ignored
expect(parsedUpdates).not.toEqual(
expect.arrayContaining([
{
kind: "N",
path: ["Text1", "label"],
rhs: UPDATED_LABEL,
},
]),
);
});
test("should generate updates based on the evalOrder", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
draft.Text1.text = UPDATED_LABEL;
});
const updateTreeResponse = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
// ignore label Text1.label uneval update and just include Text1.text uneval update
// expect(updateTreeResponse.evalOrder).toEqual([]);
updateTreeResponse.evalOrder = [];
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
updateTreeResponse,
[],
[],
);
const parsedUpdates =
getParsedUpdatesFromWebWorkerResp(webworkerResponse);
// Text1.label update should be ignored
expect(parsedUpdates).not.toEqual(
expect.arrayContaining([
{
kind: "N",
path: ["Text2", "text"],
rhs: "updated Label",
},
]),
);
});
test("should generate the correct updates to be sent to the main thread's state when the value tied to a binding changes ", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
if (draft.Text1?.text) {
draft.Text1.text = UPDATED_LABEL;
}
});
const updateTreeResponse = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
updateTreeResponse,
[],
[],
);
const parsedUpdates =
getParsedUpdatesFromWebWorkerResp(webworkerResponse);
expect(parsedUpdates).toEqual(
expect.arrayContaining([
{
kind: "N",
path: ["Text1", "text"],
rhs: "updated Label",
},
{
kind: "N",
path: ["Text2", "text"],
rhs: "updated Label",
},
]),
);
expect(evaluator.evalTree).toHaveProperty("Text2.text", UPDATED_LABEL);
});
test("should merge additional updates to the dataTree as well as push the updates back to the main thread's state when unEvalUpdates is ignored", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
if (draft.Text1?.text) {
draft.Text1.text = UPDATED_LABEL;
}
});
const updateTreeResponse = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
//set the unEvalUpdates is empty so that evaluation ignores diffing the node
updateTreeResponse.unEvalUpdates = [];
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
updateTreeResponse,
[],
["Text1.text"],
);
const parsedUpdates =
getParsedUpdatesFromWebWorkerResp(webworkerResponse);
expect(parsedUpdates).toEqual(
expect.arrayContaining([
{
kind: "N",
path: ["Text1", "text"],
rhs: UPDATED_LABEL,
},
]),
);
expect(evaluator.evalTree).toHaveProperty("Text1.text", UPDATED_LABEL);
});
});
describe("evalMetaUpdates", () => {
test("should add metaUpdates in the webworker's response", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
if (draft.Text1?.text) {
draft.Text1.text = UPDATED_LABEL;
}
});
const response = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
const metaUpdates = [
{
widgetId: unEvalTree.Text1.widgetId,
metaPropertyPath: ["someMetaValuePath"],
value: "someValue",
},
];
const { workerResponse } =
evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
response,
metaUpdates,
[],
);
expect(workerResponse.evalMetaUpdates).toEqual(metaUpdates);
});
test("should sanitise metaUpdates in the webworker's response and strip out non serialisable properties", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
if (draft.Text1?.text) {
draft.Text1.text = UPDATED_LABEL;
}
});
const response = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
const metaUpdates = [
{
widgetId: unEvalTree.Text1.widgetId,
metaPropertyPath: ["someMetaValuePath"],
value: function () {},
},
];
const { workerResponse } =
evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
response,
metaUpdates,
[],
);
// the function properties should be stripped out
expect(workerResponse.evalMetaUpdates).toEqual([
{
widgetId: unEvalTree.Text1.widgetId,
metaPropertyPath: ["someMetaValuePath"],
},
]);
});
});
describe("unEvalUpdates", () => {
test("should add unEvalUpdates to the web worker response", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
if (draft.Text1?.text) {
draft.Text1.text = UPDATED_LABEL;
}
});
const updateTreeResponse = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
updateTreeResponse,
[],
[],
);
const parsedUpdates =
getParsedUpdatesFromWebWorkerResp(webworkerResponse);
expect(webworkerResponse.workerResponse.unEvalUpdates).toEqual([
{
event: DataTreeDiffEvent.NOOP,
payload: { propertyPath: "Text1.text", value: "" },
},
]);
expect(parsedUpdates).toEqual(
expect.arrayContaining([
{
kind: "N",
path: ["Text1", "text"],
rhs: UPDATED_LABEL,
},
]),
);
});
test("should ignore generating updates when unEvalUpdates is empty", () => {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updatedLabelUnevalTree = create(unEvalTree, (draft: any) => {
if (draft.Text1?.text) {
draft.Text1.text = UPDATED_LABEL;
}
});
const updateTreeResponse = evaluator.setupUpdateTree(
updatedLabelUnevalTree,
configTree,
);
//set the evalOrder is empty so that evaluation ignores diffing the node
updateTreeResponse.unEvalUpdates = [];
const webworkerResponse = evalTreeWithChanges.evaluateAndGenerateResponse(
evaluator,
updateTreeResponse,
[],
[],
);
const parsedUpdates =
getParsedUpdatesFromWebWorkerResp(webworkerResponse);
expect(parsedUpdates).not.toEqual(
expect.arrayContaining([
{
kind: "N",
path: ["Text1", "text"],
rhs: UPDATED_LABEL,
},
]),
);
});
});
});