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.
This commit is contained in:
undergroundwires
2023-10-18 16:44:49 +02:00
parent 98a26f9ae4
commit 79b46bf210
5 changed files with 202 additions and 124 deletions

View File

@@ -1,29 +0,0 @@
import { ReadOnlyTreeNode } from '../../Node/TreeNode';
import { RenderQueueOrderer } from './RenderQueueOrderer';
export class CollapseDepthOrderer implements RenderQueueOrderer {
public orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[] {
return orderNodes(nodes);
}
}
function orderNodes(nodes: Iterable<ReadOnlyTreeNode>): 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;
}

View File

@@ -0,0 +1,35 @@
import { ReadOnlyTreeNode } from '../../Node/TreeNode';
import { RenderQueueOrderer } from './RenderQueueOrderer';
export class CollapsedParentOrderer implements RenderQueueOrderer {
public orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[] {
return orderNodes(nodes);
}
}
function orderNodes(nodes: Iterable<ReadOnlyTreeNode>): 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;
}

View File

@@ -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<ReadOnlyTreeNode>();
const nodesBeingRendered = shallowRef(new Set<ReadOnlyTreeNode>());

View File

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

View File

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