PromucFlow_constructor/app/client/src/workers/Evaluation/helpers.ts

276 lines
8.5 KiB
TypeScript
Raw Normal View History

chore: send diff updates from worker (#24933) ## Description - Optimisation around evaluation updates to the state - Updates generation logic moved from main thread to worker thread - The diff between previous state and next state is less exacting to limit the number of updates - Logic to compress similar updates to reduce the diff updates sent from worker thread to main thread - Memoisation fixes and some selector optimisation for improved performance. #### PR fixes following issue(s) Fixes #24866 #### Type of change - Chore (housekeeping or task changes that don't impact user perception) ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [x] Manual - [x] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
2023-08-16 05:34:32 +00:00
import type { Diff } from "deep-diff";
import { diff } from "deep-diff";
import type { DataTree } from "entities/DataTree/dataTreeFactory";
import equal from "fast-deep-equal";
import produce from "immer";
import { get, isNumber, isObject, set } from "lodash";
export interface DiffReferenceState {
kind: "referenceState";
path: any[];
referencePath: string;
}
export type DiffWithReferenceState =
| Diff<DataTree, DataTree>
| DiffReferenceState;
// Finds the first index which is a duplicate value
// Returns -1 if there are no duplicates
// Returns the index of the first duplicate entry it finds
// Note: This "can" fail if the object entries don't have their properties in the
// same order.
export const findDuplicateIndex = (arr: Array<unknown>) => {
const _uniqSet = new Set();
let currSetSize = 0;
for (let i = 0; i < arr.length; i++) {
// JSON.stringify because value can be objects
_uniqSet.add(JSON.stringify(arr[i]));
if (_uniqSet.size > currSetSize) currSetSize = _uniqSet.size;
else return i;
}
return -1;
};
2022-03-22 06:09:28 +00:00
/** Function that count occurrences of a substring in a string;
* @param {String} string The string
* @param {String} subString The sub string to search for
* @param {Boolean} [allowOverlapping] Optional. (Default:false)
* @param {Number | null} [maxLimit] Optional. (Default:null)
*/
export const countOccurrences = (
string: string,
subString: string,
allowOverlapping = false,
maxLimit: number | null = null,
): number => {
string += "";
subString += "";
if (subString.length <= 0) return string.length + 1;
let n = 0, // count of occurrences
pos = 0; // current position of the pointer
const step = allowOverlapping ? 1 : subString.length;
while (true) {
pos = string.indexOf(subString, pos);
if (pos >= 0) {
++n;
/**
* If you are only interested in knowing
* whether occurances count exceeds maxLimit,
* then break the loop.
*/
if (maxLimit && n > maxLimit) break;
pos += step;
} else break;
}
return n;
};
chore: send diff updates from worker (#24933) ## Description - Optimisation around evaluation updates to the state - Updates generation logic moved from main thread to worker thread - The diff between previous state and next state is less exacting to limit the number of updates - Logic to compress similar updates to reduce the diff updates sent from worker thread to main thread - Memoisation fixes and some selector optimisation for improved performance. #### PR fixes following issue(s) Fixes #24866 #### Type of change - Chore (housekeeping or task changes that don't impact user perception) ## Testing > #### How Has This Been Tested? > Please describe the tests that you ran to verify your changes. Also list any relevant details for your test configuration. > Delete anything that is not relevant - [x] Manual - [x] Jest - [ ] Cypress > > #### Test Plan > Add Testsmith test cases links that relate to this PR > > #### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) > > > ## Checklist: #### Dev activity - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag #### QA activity: - [ ] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-) have been covered - [ ] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [ ] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [ ] Cypress test cases have been added and approved by SDET/manual QA - [ ] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
2023-08-16 05:34:32 +00:00
const LARGE_COLLECTION_SIZE = 100;
// for object paths which have a "." in the object key like "a.['b.c']"
const REGEX_NESTED_OBJECT_PATH = /(.+)\.\[\'(.*)\'\]/;
const generateWithKey = (basePath: any, key: any) => {
const segmentedPath = [...basePath, key];
if (isNumber(key)) {
return {
path: basePath.join(".") + ".[" + key + "]",
segmentedPath,
};
}
if (key.includes(".")) {
return {
path: basePath.join(".") + ".['" + key + "']",
segmentedPath,
};
}
return {
path: basePath.join(".") + "." + key,
segmentedPath,
};
};
const isLargeCollection = (val: any) => {
if (!Array.isArray(val)) return false;
const rowSize = !isObject(val[0]) ? 1 : Object.keys(val[0]).length;
const size = val.length * rowSize;
return size > LARGE_COLLECTION_SIZE;
};
const normaliseEvalPath = (identicalEvalPathsPatches: any) =>
Object.keys(identicalEvalPathsPatches || {}).reduce(
(acc: any, evalPath: string) => {
//for object paths which have a "." in the object key like "a.['b.c']", we need to extract these
// paths and break them to appropriate patch paths
const matches = evalPath.match(REGEX_NESTED_OBJECT_PATH);
if (!matches || !matches.length) {
//regular paths like "a.b.c"
acc[evalPath] = identicalEvalPathsPatches[evalPath];
return acc;
}
const [, firstSeg, nestedPathSeg] = matches;
// normalise non nested paths like "a.['b']"
if (!nestedPathSeg.includes(".")) {
const key = [firstSeg, nestedPathSeg].join(".");
acc[key] = identicalEvalPathsPatches[evalPath];
return acc;
}
// object paths which have a "." like "a.['b.c']"
const key = [firstSeg, `['${nestedPathSeg}']`].join(".");
acc[key] = identicalEvalPathsPatches[evalPath];
return acc;
},
{},
);
//completely new updates which the diff will not traverse through needs to be attached
const generateMissingSetPathsUpdates = (
ignoreLargeKeys: any,
ignoreLargeKeysHasBeenAttached: any,
dataTree: any,
): DiffWithReferenceState[] =>
Object.keys(ignoreLargeKeys)
.filter((evalPath) => !ignoreLargeKeysHasBeenAttached.has(evalPath))
.map((evalPath) => {
const statePath = ignoreLargeKeys[evalPath];
//for object paths which have a "." in the object key like "a.['b.c']", we need to extract these
// paths and break them to appropriate patch paths
//get the matching value from the widget properies in the data tree
const val = get(dataTree, statePath);
const matches = evalPath.match(REGEX_NESTED_OBJECT_PATH);
if (!matches || !matches.length) {
//regular paths like "a.b.c"
return {
kind: "N",
path: evalPath.split("."),
rhs: val,
};
}
// object paths which have a "." like "a.['b.c']"
const [, firstSeg, nestedPathSeg] = matches;
const segmentedPath = [...firstSeg.split("."), nestedPathSeg];
return {
kind: "N",
path: segmentedPath,
rhs: val,
};
});
const generateDiffUpdates = (
oldDataTree: any,
dataTree: any,
ignoreLargeKeys: any,
): DiffWithReferenceState[] => {
const attachDirectly: DiffWithReferenceState[] = [];
const ignoreLargeKeysHasBeenAttached = new Set();
const attachLater: DiffWithReferenceState[] = [];
const updates =
diff(oldDataTree, dataTree, (path, key) => {
if (!path.length || key === "__evaluation__") return false;
const { path: setPath, segmentedPath } = generateWithKey(path, key);
// if ignore path is present...this segment of code generates the data compression patches
if (!!ignoreLargeKeys[setPath]) {
const originalStateVal = get(oldDataTree, segmentedPath);
const correspondingStatePath = ignoreLargeKeys[setPath];
const statePathValue = get(dataTree, correspondingStatePath);
if (!equal(originalStateVal, statePathValue)) {
//reference state patches are a patch that does not have a patch value but it provides a path which contains the same value
//this is helpful in making the payload sent to the main thread small
attachLater.push({
kind: "referenceState",
path: segmentedPath,
referencePath: correspondingStatePath,
});
}
ignoreLargeKeysHasBeenAttached.add(setPath);
return true;
}
const rhs = get(dataTree, segmentedPath);
const lhs = get(oldDataTree, segmentedPath);
const isLhsLarge = isLargeCollection(lhs);
const isRhsLarge = isLargeCollection(rhs);
if (!isLhsLarge && !isRhsLarge) {
//perform diff on this node
return false;
}
//if either of values are large just directly attach it don't have to generate very granular updates
if ((!isLhsLarge && isRhsLarge) || (isLhsLarge && !isRhsLarge)) {
attachDirectly.push({ kind: "N", path: segmentedPath, rhs });
return true;
}
//if the values are different attach the update directly
!equal(lhs, rhs) &&
attachDirectly.push({ kind: "N", path: segmentedPath, rhs });
//ignore diff on this node
return true;
}) || [];
const missingSetPaths = generateMissingSetPathsUpdates(
ignoreLargeKeys,
ignoreLargeKeysHasBeenAttached,
dataTree,
);
const largeDataSetUpdates = [
...attachDirectly,
...missingSetPaths,
...attachLater,
];
return [...updates, ...largeDataSetUpdates];
};
export const generateOptimisedUpdates = (
oldDataTree: any,
dataTree: any,
identicalEvalPathsPatches?: Record<string, string>,
): DiffWithReferenceState[] => {
const ignoreLargeKeys = normaliseEvalPath(identicalEvalPathsPatches);
const updates = generateDiffUpdates(oldDataTree, dataTree, ignoreLargeKeys);
return updates;
};
export const decompressIdenticalEvalPaths = (
dataTree: any,
identicalEvalPathsPatches: Record<string, string>,
) =>
produce(dataTree, (draft: any) =>
Object.entries(identicalEvalPathsPatches || {}).forEach(([key, value]) => {
const referencePathValue = get(dataTree, value);
set(draft, key, referencePathValue);
}),
);
export const generateOptimisedUpdatesAndSetPrevState = (
dataTree: any,
dataTreeEvaluator: any,
) => {
const identicalEvalPathsPatches =
dataTreeEvaluator?.getEvalPathsIdenticalToState();
const updates = generateOptimisedUpdates(
dataTreeEvaluator?.getPrevState(),
dataTree,
identicalEvalPathsPatches,
);
dataTreeEvaluator?.setPrevState(dataTree);
return updates;
};