Fix slow appearance of nodes on tree view
The tree view rendering performance is optimized by improving the node render queue ordering. The node rendering order is modified based on the expansion state and the depth in the hierarchy, leading to faster rendering of visible nodes. This optimization is applied when the tree nodes are not expanded to improve the rendering speed. This new ordering ensures that nodes are rendered more efficiently, prioritizing nodes that are collapsed and are at a higher level in the hierarchy.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TimeFunctions, TimeoutDelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/TimeoutDelayScheduler';
|
||||
import { TimeFunctions, TimeoutDelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Scheduling/TimeoutDelayScheduler';
|
||||
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
|
||||
|
||||
describe('TimeoutDelayScheduler', () => {
|
||||
@@ -12,7 +12,9 @@ import { NodeStateChangeEventArgsStub } from '@tests/unit/shared/Stubs/NodeState
|
||||
import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeStateDescriptorStub';
|
||||
import { DelaySchedulerStub } from '@tests/unit/shared/Stubs/DelaySchedulerStub';
|
||||
import { DelayScheduler } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/DelayScheduler';
|
||||
import { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { ReadOnlyTreeNode, TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { RenderQueueOrdererStub } from '@tests/unit/shared/Stubs/RenderQueueOrdererStub';
|
||||
import { RenderQueueOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer';
|
||||
|
||||
describe('useGradualNodeRendering', () => {
|
||||
it('watches nodes on specified tree', () => {
|
||||
@@ -194,6 +196,42 @@ describe('useGradualNodeRendering', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
it('orders nodes before rendering', async () => {
|
||||
// arrange
|
||||
const delaySchedulerStub = new DelaySchedulerStub();
|
||||
const allNodes = Array.from({ length: 3 }).map(() => createNodeWithVisibility(true));
|
||||
const expectedNodes = [
|
||||
/* initial render */ [allNodes[2]],
|
||||
/* first subsequent render */ [allNodes[1]],
|
||||
/* second subsequent render */ [allNodes[0]],
|
||||
];
|
||||
const ordererStub = new RenderQueueOrdererStub();
|
||||
const nodesStub = new UseCurrentTreeNodesStub().withQueryableNodes(
|
||||
new QueryableNodesStub().withFlattenedNodes(allNodes),
|
||||
);
|
||||
const builder = new UseGradualNodeRenderingBuilder()
|
||||
.withCurrentTreeNodes(nodesStub)
|
||||
.withInitialBatchSize(1)
|
||||
.withSubsequentBatchSize(1)
|
||||
.withDelayScheduler(delaySchedulerStub)
|
||||
.withOrderer(ordererStub);
|
||||
const actualOrder = new Set<ReadOnlyTreeNode>();
|
||||
// act
|
||||
ordererStub.orderNodes = () => expectedNodes[0];
|
||||
const strategy = builder.call();
|
||||
const updateOrder = () => allNodes
|
||||
.filter((node) => strategy.shouldRender(node))
|
||||
.forEach((node) => actualOrder.add(node));
|
||||
updateOrder();
|
||||
for (let i = 1; i < expectedNodes.length; i++) {
|
||||
ordererStub.orderNodes = () => expectedNodes[i];
|
||||
delaySchedulerStub.runNextScheduled();
|
||||
updateOrder();
|
||||
}
|
||||
// assert
|
||||
const expectedOrder = expectedNodes.flat();
|
||||
expect([...actualOrder]).to.deep.equal(expectedOrder);
|
||||
});
|
||||
});
|
||||
it('skips scheduling when no nodes to render', () => {
|
||||
// arrange
|
||||
@@ -241,6 +279,8 @@ class UseGradualNodeRenderingBuilder {
|
||||
|
||||
private subsequentBatchSize = 3;
|
||||
|
||||
private orderer: RenderQueueOrderer = new RenderQueueOrdererStub();
|
||||
|
||||
public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this {
|
||||
this.changeAggregator = changeAggregator;
|
||||
return this;
|
||||
@@ -271,6 +311,11 @@ class UseGradualNodeRenderingBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withOrderer(orderer: RenderQueueOrderer) {
|
||||
this.orderer = orderer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public call(): ReturnType<typeof useGradualNodeRendering> {
|
||||
return useGradualNodeRendering(
|
||||
this.treeWatcher,
|
||||
@@ -279,6 +324,7 @@ class UseGradualNodeRenderingBuilder {
|
||||
this.delayScheduler,
|
||||
this.initialBatchSize,
|
||||
this.subsequentBatchSize,
|
||||
this.orderer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user