chore: optimised isChildPropertyPath to not use a regex and added more logging around calculateSubTreeSortOrder (#41162)

## Description
Reduced the cumulative contribution of isChildPropertyPath by
approximately 98%. During page load, it originally took around 100 ms
for a customer app on a Mac machine and is now down to 2 ms. As a
result, calculateSubTreeSortOrder has improved by 70% on the same setup.
Optimised sorting and removed redundant lookups in addNodes, which led
to marginal gains. This optimisation specifically targets a customer
scenario where addNodes and addDependantsOfNestedPropertyPaths are
heavily stressed, contributing to an overall latency of about 7 seconds.
Added additional logging to investigate the issue further.


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/16714192460>
> Commit: d6633bb07190c897a9a9d9563e606c4dd220fa55
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=16714192460&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Mon, 04 Aug 2025 05:57:55 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 Features**
* Added a new function to improve detection of child property paths
supporting both dot and bracket notation.

* **Refactor**
* Optimized internal logic for managing dependency sets and improved
node addition efficiency.
  * Updated sorting method to accept arrays for better consistency.

* **Style**
* Enhanced code readability and maintainability with more concise
patterns.

* **Chores**
* Introduced performance timing and logging for key operations to aid in
monitoring and diagnostics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Vemparala Surya Vamsi 2025-08-04 11:32:15 +05:30 committed by GitHub
parent d0b88994aa
commit 5d38e47508
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 100 additions and 28 deletions

View File

@ -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),

View File

@ -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, 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,
}));
@ -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.
@ -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);

View File

@ -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)
);
};

View File

@ -1958,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);
@ -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(