How to Prevent Concurrent Rendering Bugs in OpenSumi’s File Tree
This article analyzes the concurrent rendering problems that arise in OpenSumi’s file‑tree after removing throttling, explains their root causes, and presents a comprehensive solution—including operation prioritization, cancellable actions, and queued updates—to achieve both high performance and stable user interaction.
Introduction
In the OpenSumi framework, all Tree components use a flat rendering structure. Removing throttling to speed up response (under 100 ms) caused a surge of concurrent rendering requests in the file‑tree, leading to rendering anomalies.
Problems
Duplicate Nodes
Repeated rendering nodes appear, especially when paths are compressed (e.g., a/b/c/d).
Refresh Conflicts
Frequent file‑tree refreshes overlap with user operations, causing the view to “jump” back and forth. For example, creating a file every 200 ms triggers a refresh that can degrade performance on slower machines.
Root Cause Analysis
The issues stem from two problems: (1) TreeModel’s node list being incorrectly trimmed or extended, and (2) TreeNode’s cached nodes not being updated properly.
Data updates in TreeModel cannot be cancelled; inserting a view update during a data update corrupts the rendering.
Operation priority should be: collapse > expand > node locate > refresh.
Solution
1. Separate Core and Non‑Core Operations
Core operations (expand/collapse) must cancel non‑core ones (snapshot restore, locate, refresh).
Conflict Handling
Expanding a node while a refresh is in progress.
Locating a node while a refresh is in progress.
Locating then expanding during a refresh.
Locating, expanding, then immediately collapsing during a refresh.
2. Support Cancellation Before Rendering
Make file‑tree actions interruptible by passing a CancellationToken to the refresh logic.
class TreeNode {<br/> ...<br/> refresh(<br/> expandedPaths: string[] = this.getAllExpandedNodePath(),<br/> token?: CancellationToken,<br/> ) {<br/> const childrens = (await this._tree.resolveChildren(this)) || [];<br/> if (token?.isCancellationRequested) { return; }<br/> while ((forceLoadPath = expandedPaths.shift())) {<br/> const child = childrens?.find(child => child.path === forceLoadPath);<br/> if (CompositeTreeNode.is(child)) {<br/> await child.resolveChildrens();<br/> if (token?.isCancellationRequested) { return; }<br/> await child.refresh(expandedPaths, token);<br/> if (token?.isCancellationRequested) { return; }<br/> }<br/> }<br/> if (forceLoadPath) {<br/> expandedPaths.unshift(forceLoadPath);<br/> this.expandBranch(this, true);<br/> } else if (CompositeTreeNode.isRoot(this)) {<br/> const expandedChilds: CompositeTreeNode[] = [];<br/> const flatTree = new Array(childrens.length);<br/> this._children = Array(childrens.length);<br/> for (let i = 0; i < childrens.length; i++) {<br/> const child = childrens[i];<br/> this._children[i] = child;<br/> if (CompositeTreeNode.is(child) && child.expanded) { expandedChilds.push(child); }<br/> }<br/> this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);<br/> for (let i = 0; i < childrens.length; i++) { flatTree[i] = this._children[i].id; }<br/> this._branchSize = flatTree.length;<br/> this.setFlattenedBranch(flatTree, true);<br/> for (let i = 0; i < expandedChilds.length; i++) {<br/> const child = expandedChilds[i];<br/> child.expandBranch(child, true);<br/> }<br/> }<br/> }<br/> ...<br/>}3. Queue Data Updates and Rendering
Ensure that a refresh finishes before the next one starts; pending refreshes are merged into a single update, simplifying the implementation and improving performance.
Implementation Details
The refresh logic now stores pending children in a temporary property, merges them after all children load, and respects the cancellation token.
class TreeNode {<br/> ...<br/> refresh(<br/> expandedPaths: string[] = this.getAllExpandedNodePath(),<br/> token?: CancellationToken,<br/> ) {<br/> const childrens = (await this._tree.resolveChildren(this)) || [];<br/> // ... (same as above) ...<br/> }<br/> ...<br/>}A global map tracks the state of each tree path, allowing any operation to cancel ongoing refreshes or node‑loading tasks.
class TreeNode {<br/> public static pathToGlobalTreeState: Map<string, IGlobalTreeState> = new Map();<br/> public static getGlobalTreeState(path: string) {<br/> const root = path.split(Path.separator).slice(0, 2).join(Path.separator);<br/> let state = TreeNode.pathToGlobalTreeState.get(root);<br/> if (!state) {<br/> state = {<br/> isExpanding: false,<br/> isLoadingPath: false,<br/> isRefreshing: false,<br/> refreshCancelToken: new CancellationTokenSource(),<br/> loadPathCancelToken: new CancellationTokenSource(),<br/> };<br/> }<br/> return state;<br/> }<br/> public static setGlobalTreeState(path: string, updateState: IOptionalGlobalTreeState) {<br/> const root = path.split(Path.separator).slice(0, 2).join(Path.separator);<br/> let state = TreeNode.pathToGlobalTreeState.get(root) || {<br/> isExpanding: false,<br/> isLoadingPath: false,<br/> isRefreshing: false,<br/> refreshCancelToken: new CancellationTokenSource(),<br/> loadPathCancelToken: new CancellationTokenSource(),<br/> };<br/> state = { ...state, ...updateState };<br/> TreeNode.pathToGlobalTreeState.set(root, state);<br/> return state;<br/> }<br/> ...<br/>}When expanding a node, the method cancels any ongoing locate or refresh tasks, updates the global state, and proceeds with the expansion.
public async setExpanded(ensureVisible = true, quiet = false, isOwner = true, token?: CancellationToken) {<br/> const state = TreeNode.getGlobalTreeState(this.path);<br/> state.loadPathCancelToken.cancel();<br/> state.refreshCancelToken.cancel();<br/> TreeNode.setGlobalTreeState(this.path, { isExpanding: true });<br/> this.isExpanded = true;<br/> if (this._children === null) {<br/> await this.hardReloadChildren(token);<br/> }<br/> ...<br/>}Result
Under frequent file changes, the file‑tree remains responsive, preserving both operation continuity and real‑time state updates.
Conclusion
The file‑tree case demonstrates that achieving high performance and smooth interaction requires careful handling of concurrent operations, proper prioritization, and cancellable updates; the presented approach resolves the stability and performance issues in OpenSumi’s file‑tree.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
