From 79b46bf21004d96d31551439e5db5d698a3f71f3 Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Wed, 18 Oct 2023 16:44:49 +0200 Subject: [PATCH] Improve performance of rendering during search Optimize the tree view rendering during searches by enhancing the render queue ordering. This update changes the rendering order to prioritize visible nodes, leading to faster appearance of these nodes during searches. The ordering logic now ignores the depth in the hierarchy and instead focused on the node order. The collapsed check for the node itself is removed, ensuring that visible collapsed parents are first while their invisible children are rendered later. --- .../Ordering/CollapseDepthOrderer.ts | 29 --- .../Ordering/CollapsedParentOrderer.ts | 35 ++++ .../Rendering/UseGradualNodeRendering.ts | 4 +- .../Ordering/CollapseDepthOrderer.spec.ts | 93 ---------- .../Ordering/CollapsedParentOrderer.spec.ts | 165 ++++++++++++++++++ 5 files changed, 202 insertions(+), 124 deletions(-) delete mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.ts create mode 100644 src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts delete mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.spec.ts create mode 100644 tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.ts deleted file mode 100644 index 4ea614db..00000000 --- a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ReadOnlyTreeNode } from '../../Node/TreeNode'; -import { RenderQueueOrderer } from './RenderQueueOrderer'; - -export class CollapseDepthOrderer implements RenderQueueOrderer { - public orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { - return orderNodes(nodes); - } -} - -function orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { - return [...nodes] - .sort((a, b) => { - const [aCollapseStatus, bCollapseStatus] = [isNodeCollapsed(a), isNodeCollapsed(b)]; - if (aCollapseStatus !== bCollapseStatus) { - return (aCollapseStatus ? 1 : 0) - (bCollapseStatus ? 1 : 0); - } - return a.hierarchy.depthInTree - b.hierarchy.depthInTree; - }); -} - -function isNodeCollapsed(node: ReadOnlyTreeNode): boolean { - if (!node.state.current.isExpanded) { - return true; - } - if (node.hierarchy.parent) { - return isNodeCollapsed(node.hierarchy.parent); - } - return false; -} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts new file mode 100644 index 00000000..0f1fc854 --- /dev/null +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.ts @@ -0,0 +1,35 @@ +import { ReadOnlyTreeNode } from '../../Node/TreeNode'; +import { RenderQueueOrderer } from './RenderQueueOrderer'; + +export class CollapsedParentOrderer implements RenderQueueOrderer { + public orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { + return orderNodes(nodes); + } +} + +function orderNodes(nodes: Iterable): ReadOnlyTreeNode[] { + return [...nodes] + .map((node, index) => ({ node, index })) + .sort((a, b) => { + const [ + isANodeOfCollapsedParent, + isBNodeOfCollapsedParent, + ] = [isParentCollapsed(a.node), isParentCollapsed(b.node)]; + if (isANodeOfCollapsedParent !== isBNodeOfCollapsedParent) { + return (isANodeOfCollapsedParent ? 1 : 0) - (isBNodeOfCollapsedParent ? 1 : 0); + } + return a.index - b.index; + }) + .map(({ node }) => node); +} + +function isParentCollapsed(node: ReadOnlyTreeNode): boolean { + const parentNode = node.hierarchy.parent; + if (parentNode) { + if (!parentNode.state.current.isExpanded) { + return true; + } + return isParentCollapsed(parentNode); + } + return false; +} diff --git a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts index 34661719..5429c6b5 100644 --- a/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts +++ b/src/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering.ts @@ -9,7 +9,7 @@ import { NodeRenderingStrategy } from './Scheduling/NodeRenderingStrategy'; import { DelayScheduler } from './DelayScheduler'; import { TimeoutDelayScheduler } from './Scheduling/TimeoutDelayScheduler'; import { RenderQueueOrderer } from './Ordering/RenderQueueOrderer'; -import { CollapseDepthOrderer } from './Ordering/CollapseDepthOrderer'; +import { CollapsedParentOrderer } from './Ordering/CollapsedParentOrderer'; /** * Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes. @@ -21,7 +21,7 @@ export function useGradualNodeRendering( scheduler: DelayScheduler = new TimeoutDelayScheduler(), initialBatchSize = 30, subsequentBatchSize = 5, - orderer: RenderQueueOrderer = new CollapseDepthOrderer(), + orderer: RenderQueueOrderer = new CollapsedParentOrderer(), ): NodeRenderingStrategy { const nodesToRender = new Set(); const nodesBeingRendered = shallowRef(new Set()); diff --git a/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.spec.ts b/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.spec.ts deleted file mode 100644 index df6436e8..00000000 --- a/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode'; -import { CollapseDepthOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapseDepthOrderer'; -import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub'; -import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub'; -import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub'; -import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub'; - -describe('CollapseDepthOrderer', () => { - describe('orderNodes', () => { - it('should order by collapsed state and then by depth in', () => { - // arrange - const node1 = createNodeForOrder({ - isExpanded: false, - depthInTree: 1, - }); - const node2 = createNodeForOrder({ - isExpanded: true, - depthInTree: 2, - }); - const node3 = createNodeForOrder({ - isExpanded: false, - depthInTree: 3, - }); - const node4 = createNodeForOrder({ - isExpanded: false, - depthInTree: 4, - }); - const nodes = [node1, node2, node3, node4]; - const expectedOrder = [node2, node1, node3, node4]; - // act - const orderer = new CollapseDepthOrderer(); - const orderedNodes = orderer.orderNodes(nodes); - // assert - expect(orderedNodes.map((node) => node.id)).to.deep - .equal(expectedOrder.map((node) => node.id)); - }); - it('should handle parent collapsed state', () => { - // arrange - const collapsedParent = createNodeForOrder({ - isExpanded: false, - depthInTree: 0, - }); - const childWithCollapsedParent = createNodeForOrder({ - isExpanded: true, - depthInTree: 1, - parent: collapsedParent, - }); - const deepExpandedNode = createNodeForOrder({ - isExpanded: true, - depthInTree: 3, - }); - const nodes = [childWithCollapsedParent, collapsedParent, deepExpandedNode]; - const expectedOrder = [ - deepExpandedNode, // comes first due to collapse parent of child - collapsedParent, - childWithCollapsedParent, - ]; - // act - const orderer = new CollapseDepthOrderer(); - const orderedNodes = orderer.orderNodes(nodes); - // assert - expect(orderedNodes.map((node) => node.id)).to.deep - .equal(expectedOrder.map((node) => node.id)); - }); - }); -}); - -function createNodeForOrder(options: { - readonly isExpanded: boolean; - readonly depthInTree: number; - readonly parent?: TreeNode; -}): TreeNode { - return new TreeNodeStub() - .withId([ - `isExpanded: ${options.isExpanded}`, - `depthInTree: ${options.depthInTree}`, - ...(options.parent ? [`parent: ${options.parent.id}`] : []), - ].join(', ')) - .withState( - new TreeNodeStateAccessStub() - .withCurrent( - new TreeNodeStateDescriptorStub() - .withVisibility(true) - .withExpansion(options.isExpanded), - ), - ) - .withHierarchy( - new HierarchyAccessStub() - .withDepthInTree(options.depthInTree) - .withParent(options.parent), - ); -} diff --git a/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts b/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts new file mode 100644 index 00000000..ab28742e --- /dev/null +++ b/tests/unit/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer.spec.ts @@ -0,0 +1,165 @@ +import { describe, it, expect } from 'vitest'; +import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode'; +import { CollapsedParentOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/CollapsedParentOrderer'; +import { HierarchyAccessStub } from '@tests/unit/shared/Stubs/HierarchyAccessStub'; +import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub'; +import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub'; +import { TreeNodeStub } from '@tests/unit/shared/Stubs/TreeNodeStub'; + +describe('CollapsedParentOrderer', () => { + describe('orderNodes', () => { + const scenarios: ReadonlyArray<{ + readonly description: string; + readonly nodes: TreeNode[]; + readonly expectedOrderedNodes: TreeNode[]; + }> = [ + { + description: 'handles empty nodes list', + nodes: [], + expectedOrderedNodes: [], + }, + (() => { + const expectedNode = createNodeForOrder({ + isExpanded: false, + }); + return { + description: 'handles single node list', + nodes: [expectedNode], + expectedOrderedNodes: [expectedNode], + }; + })(), + (() => { + const node1 = createNodeForOrder({ + isExpanded: false, + }); + const node2 = createNodeForOrder({ + isExpanded: true, + }); + const node3 = createNodeForOrder({ + isExpanded: true, + }); + const node4 = createNodeForOrder({ + isExpanded: false, + }); + return { + description: 'orders by index ignoring self collapsed state', + nodes: [node1, node2, node3, node4], + expectedOrderedNodes: [node1, node2, node3, node4], + }; + })(), + (() => { + const node1 = createNodeForOrder({ + isExpanded: false, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node2 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node3 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node4 = createNodeForOrder({ + isExpanded: false, + parent: createNodeForOrder({ isExpanded: true }), + }); + return { + description: 'orders by index if all parents are expanded', + nodes: [node1, node2, node3, node4], + expectedOrderedNodes: [node1, node2, node3, node4], + }; + })(), + (() => { + const node1 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: false }), + }); + const node2 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: true }), + }); + const node3 = createNodeForOrder({ + isExpanded: true, + }); + const node4 = createNodeForOrder({ + isExpanded: true, + parent: createNodeForOrder({ isExpanded: false }), + }); + return { + description: 'order by parent collapsed state then by index', + nodes: [node1, node2, node3, node4], + expectedOrderedNodes: [node2, node3, node1, node4], + }; + })(), + (() => { + const collapsedNode = createNodeForOrder({ + isExpanded: false, + }); + const collapsedNodeChild = createNodeForOrder({ + isExpanded: true, + parent: collapsedNode, + }); + const collapsedNodeNestedChild = createNodeForOrder({ + isExpanded: true, + parent: collapsedNodeChild, + }); + const expandedNode = createNodeForOrder({ + isExpanded: true, + }); + const expandedNodeChild = createNodeForOrder({ + isExpanded: true, + parent: expandedNode, + }); + const expandedNodeNestedChild = createNodeForOrder({ + isExpanded: true, + parent: expandedNodeChild, + }); + return { + description: 'should handle deep parent collapsed state', + nodes: [ + collapsedNode, collapsedNodeChild, collapsedNodeNestedChild, + expandedNode, expandedNodeChild, expandedNodeNestedChild, + ], + expectedOrderedNodes: [ + collapsedNode, expandedNode, expandedNodeChild, + expandedNodeNestedChild, collapsedNodeChild, collapsedNodeNestedChild, + ], + }; + })(), + ]; + scenarios.forEach(({ description, nodes, expectedOrderedNodes }) => { + it(description, () => { + // act + const orderer = new CollapsedParentOrderer(); + const orderedNodes = orderer.orderNodes(nodes); + // assert + expect(orderedNodes.map((node) => node.id)).to.deep + .equal(expectedOrderedNodes.map((node) => node.id)); + }); + }); + }); +}); + +function createNodeForOrder(options: { + readonly isExpanded: boolean; + readonly parent?: TreeNode; +}): TreeNode { + return new TreeNodeStub() + .withId([ + `isExpanded: ${options.isExpanded}`, + ...(options.parent ? [`parent: ${options.parent.id}`] : []), + ].join(', ')) + .withState( + new TreeNodeStateAccessStub() + .withCurrent( + new TreeNodeStateDescriptorStub() + .withVisibility(true) + .withExpansion(options.isExpanded), + ), + ) + .withHierarchy( + new HierarchyAccessStub() + .withParent(options.parent), + ); +}