diff --git a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts index 96c63c55d6..ddfce31052 100644 --- a/app/client/src/ce/workers/Evaluation/evaluationUtils.ts +++ b/app/client/src/ce/workers/Evaluation/evaluationUtils.ts @@ -359,11 +359,10 @@ export const addDependantsOfNestedPropertyPaths = ( parentPaths: Array, inverseMap: DependencyMap, ): Set => { - const withNestedPaths: Set = new Set(); + const withNestedPaths: Set = new Set(parentPaths); const dependantNodes = Object.keys(inverseMap); parentPaths.forEach((propertyPath) => { - withNestedPaths.add(propertyPath); dependantNodes .filter((dependantNodePath) => isChildPropertyPath(propertyPath, dependantNodePath), diff --git a/app/client/src/entities/DependencyMap/index.ts b/app/client/src/entities/DependencyMap/index.ts index dbbf6caad5..6898ce7452 100644 --- a/app/client/src/entities/DependencyMap/index.ts +++ b/app/client/src/entities/DependencyMap/index.ts @@ -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); } } @@ -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[] => { + 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, })); @@ -186,15 +192,16 @@ export default class DependencyMap { */ addNodes = (nodes: Record, 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. @@ -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); @@ -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); diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 159ce60fea..cdf099c9c8 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -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, @@ -308,8 +348,7 @@ export const isChildPropertyPath = ( ): boolean => { return ( (!strict && parentPropertyPath === childPropertyPath) || - childPropertyPath.startsWith(`${parentPropertyPath}.`) || - childPropertyPath.startsWith(`${parentPropertyPath}[`) + isChildPropertyPathStartsWithParent(parentPropertyPath, childPropertyPath) ); }; diff --git a/app/client/src/workers/common/DataTreeEvaluator/index.ts b/app/client/src/workers/common/DataTreeEvaluator/index.ts index 2955bcea47..15fe9a0b43 100644 --- a/app/client/src/workers/common/DataTreeEvaluator/index.ts +++ b/app/client/src/workers/common/DataTreeEvaluator/index.ts @@ -1958,6 +1958,8 @@ export default class DataTreeEvaluator { const changePaths: Set = new Set(dependenciesOfRemovedPaths); const configTree = this.getConfigTree(); + const updatedValuePathsLatencyStart = performance.now(); + for (const pathArray of updatedValuePaths) { const fullPropertyPath = convertPathToString(pathArray); @@ -2005,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(