Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions app/client/src/ce/workers/Evaluation/evaluationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,10 @@ export const addDependantsOfNestedPropertyPaths = (
parentPaths: Array<string>,
inverseMap: DependencyMap,
): Set<string> => {
const withNestedPaths: Set<string> = new Set();
const withNestedPaths: Set<string> = new Set(parentPaths);
const dependantNodes = Object.keys(inverseMap);

parentPaths.forEach((propertyPath) => {
withNestedPaths.add(propertyPath);
dependantNodes
.filter((dependantNodePath) =>
isChildPropertyPath(propertyPath, dependantNodePath),
Expand Down
60 changes: 37 additions & 23 deletions app/client/src/entities/DependencyMap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,26 @@ export default class DependencyMap {
if (this.#nodes.has(dependency)) {
validDependencies.add(dependency);

if (this.#dependenciesInverse.has(dependency)) {
this.#dependenciesInverse.get(dependency)?.add(node);
} else {
this.#dependenciesInverse.set(dependency, new Set([node]));
let inverseSet = this.#dependenciesInverse.get(dependency);

if (!inverseSet) {
inverseSet = new Set();
this.#dependenciesInverse.set(dependency, inverseSet);
}

inverseSet.add(node);
} else {
invalidDependencies.add(dependency);

if (this.#invalidDependenciesInverse.has(dependency)) {
this.#invalidDependenciesInverse.get(dependency)?.add(node);
} else {
this.#invalidDependenciesInverse.set(dependency, new Set([node]));
let inverseInvalidSet =
this.#invalidDependenciesInverse.get(dependency);

if (!inverseInvalidSet) {
inverseInvalidSet = new Set();
this.#invalidDependenciesInverse.set(dependency, inverseInvalidSet);
}

inverseInvalidSet.add(node);
}
}

Expand Down Expand Up @@ -163,10 +170,9 @@ export default class DependencyMap {
* @param nodes Record of nodes to sort
* @returns Array of node keys sorted by depth
*/
private sortNodesByDepth = (nodes: Record<string, true>): string[] => {
private sortNodesByDepth = (nodes: string[]): string[] => {
// Pre-compute depths for all nodes
const nodeKeys = Object.keys(nodes);
const nodeDepths = nodeKeys.map((node) => ({
const nodeDepths = nodes.map((node) => ({
node,
depth: node.split(".").length,
}));
Expand All @@ -186,15 +192,16 @@ export default class DependencyMap {
*/

addNodes = (nodes: Record<string, true>, strict = true) => {
const newUnaddedNodes = Object.keys(nodes).filter(
(node) => !this.#nodes.has(node),
);
const nodesToAdd = strict
? Object.keys(nodes)
: this.sortNodesByDepth(nodes);
? newUnaddedNodes
: this.sortNodesByDepth(newUnaddedNodes);

let didUpdateGraph = false;

for (const newNode of nodesToAdd) {
if (this.#nodes.has(newNode)) continue;

// New node introduced to the graph.
this.#nodes.set(newNode, true);
// Check the paths that consumed this node before it was introduced.
Expand Down Expand Up @@ -228,11 +235,15 @@ export default class DependencyMap {
this.#invalidDependencies.get(iNode)?.delete(newNode);
didUpdateGraph = true;

if (this.#dependenciesInverse.has(newNode)) {
this.#dependenciesInverse.get(newNode)?.add(iNode);
} else {
this.#dependenciesInverse.set(newNode, new Set([iNode]));
// Get or create the inverse dependencies set for the new node
let inverseSet = this.#dependenciesInverse.get(newNode);

if (!inverseSet) {
inverseSet = new Set();
this.#dependenciesInverse.set(newNode, inverseSet);
}

inverseSet.add(iNode);
}

this.#invalidDependenciesInverse.delete(newNode);
Expand Down Expand Up @@ -261,11 +272,14 @@ export default class DependencyMap {

if (!this.#nodes.has(iNode)) continue;

if (this.#invalidDependenciesInverse.has(node)) {
this.#invalidDependenciesInverse.get(node)?.add(iNode);
} else {
this.#invalidDependenciesInverse.set(node, new Set([iNode]));
let inverseInvalidSet = this.#invalidDependenciesInverse.get(node);

if (!inverseInvalidSet) {
inverseInvalidSet = new Set();
this.#invalidDependenciesInverse.set(node, inverseInvalidSet);
}

inverseInvalidSet.add(iNode);
}

this.#dependenciesInverse.delete(node);
Expand Down
43 changes: 41 additions & 2 deletions app/client/src/utils/DynamicBindingUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,46 @@ export const unsafeFunctionForEval = [
"Navigator",
];

/**
* Checks if a child property path starts with the parent property path
* using either dot notation (.) or bracket notation ([)
*
* @param parentPropertyPath - The parent property path
* @param childPropertyPath - The child property path to check
* @returns true if childPropertyPath is a child of parentPropertyPath
*
* @example
* isChildPropertyPathStartsWithParent("Table1", "Table1.data") // true
* isChildPropertyPathStartsWithParent("Table1", "Table1[0]") // true
* isChildPropertyPathStartsWithParent("Table1", "Table2.data") // false
*/
export const isChildPropertyPathStartsWithParent = (
parentPropertyPath: string,
childPropertyPath: string,
): boolean => {
if (!parentPropertyPath || !childPropertyPath) {
return false;
}

const parentLength = parentPropertyPath.length;

if (childPropertyPath.length <= parentLength) {
return false;
}

// Most common case: dot notation
if (childPropertyPath[parentLength] === ".") {
return childPropertyPath.startsWith(parentPropertyPath);
}

// Less common case: bracket notation
if (childPropertyPath[parentLength] === "[") {
return childPropertyPath.startsWith(parentPropertyPath);
}

return false;
};

export const isChildPropertyPath = (
parentPropertyPath: string,
childPropertyPath: string,
Expand All @@ -308,8 +348,7 @@ export const isChildPropertyPath = (
): boolean => {
return (
(!strict && parentPropertyPath === childPropertyPath) ||
childPropertyPath.startsWith(`${parentPropertyPath}.`) ||
childPropertyPath.startsWith(`${parentPropertyPath}[`)
isChildPropertyPathStartsWithParent(parentPropertyPath, childPropertyPath)
);
};

Expand Down
27 changes: 26 additions & 1 deletion app/client/src/workers/common/DataTreeEvaluator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
getAllPathsBasedOnDiffPaths,
isDataPath,
isJSModuleInstance,
isPropertyAnEntityAction,
} from "ee/workers/Evaluation/evaluationUtils";

import {
Expand Down Expand Up @@ -1192,7 +1193,11 @@ export default class DataTreeEvaluator {
valuechanged[fullPropertyPath] = true;
continue;
}

// Skip evaluations for actions in JSObjects
if (isPropertyAnEntityAction(entity, propertyPath, entityConfig)) {
continue;
}

// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -1953,6 +1958,8 @@ export default class DataTreeEvaluator {
const changePaths: Set<string> = new Set(dependenciesOfRemovedPaths);
const configTree = this.getConfigTree();

const updatedValuePathsLatencyStart = performance.now();

for (const pathArray of updatedValuePaths) {
const fullPropertyPath = convertPathToString(pathArray);

Expand Down Expand Up @@ -2000,27 +2007,45 @@ export default class DataTreeEvaluator {
}
}

const updatedValuePathsLatency =
performance.now() - updatedValuePathsLatencyStart;

// If a nested property path has changed and someone (say x) is dependent on the parent of the said property,
// x must also be evaluated. For example, the following relationship exists in dependency map:
// < "Input1.defaultText" : ["Table1.selectedRow.email"] >
// If Table1.selectedRow has changed, then Input1.defaultText must also be evaluated because Table1.selectedRow.email
// is a nested property of Table1.selectedRow
const addDependantsOfNestedPropertyPathsLatencyStart = performance.now();
const changePathsWithNestedDependants = addDependantsOfNestedPropertyPaths(
Array.from(changePaths),
this.inverseDependencies,
);

const addDependantsOfNestedPropertyPathsLatency =
performance.now() - addDependantsOfNestedPropertyPathsLatencyStart;
const trimDependantChangePathsLatencyStart = performance.now();
const trimmedChangedPaths = trimDependantChangePaths(
changePathsWithNestedDependants,
this.dependencies,
);
const trimDependantChangePathsLatency =
performance.now() - trimDependantChangePathsLatencyStart;

// Now that we have all the root nodes which have to be evaluated, recursively find all the other paths which
// would get impacted because they are dependent on the said root nodes and add them in order
const completeSortOrderLatencyStart = performance.now();
const completeSortOrder = this.getCompleteSortOrder(
trimmedChangedPaths,
this.inverseDependencies,
);
const completeSortOrderLatency =
performance.now() - completeSortOrderLatencyStart;

this.logs.push({
updatedValuePathsLatency,
addDependantsOfNestedPropertyPathsLatency,
trimDependantChangePathsLatency,
completeSortOrderLatency,
});

// Remove any paths that do not exist in the data tree anymore
return difference(
Expand Down
Loading