Resolving Concurrent Rendering Issues in OpenSumi File Tree
The article explains how OpenSumi’s file‑tree suffered duplicate nodes and jumpy refreshes after throttling removal, identifies uncancelled concurrent core and non‑core operations as the root cause, and resolves the instability by introducing cancellable tokens, operation queuing, and a global tree‑state manager to ensure only one refresh runs at a time, delivering stable, responsive rendering.
OpenSumi (https://opensumi.com) is a cross‑platform IDE development framework that supports Web, Electron and pure front‑end scenarios. It is used in enterprise products such as the Alibaba Cloud console and the Taobao mini‑program developer tools.
The article analyzes a set of concurrency rendering problems that appeared in the file‑tree component after throttling was removed in version 2.16.0 to improve response speed. The main symptoms are duplicate rendering nodes, frequent refreshes that cause the tree view to “jump”, and overall instability under high‑frequency file changes.
Problems identified :
TreeModel stores a list of rendering nodes that can be incorrectly trimmed or extended.
TreeNode caches are not updated properly, leading to stale data.
Root cause : Once a tree refresh starts, data‑update operations cannot be cancelled. If a view update is injected in the middle of a data update, the tree renders incorrectly. Moreover, core operations (expand/collapse) and non‑core operations (file location, refresh) compete for the same resources.
Solution overview :
Distinguish core and non‑core operations; cancel non‑core operations when a core operation occurs.
Make all tree operations cancellable by passing a CancellationToken to the refresh logic.
Queue data‑update and render operations so that a new refresh waits for the previous one to finish.
Maintain a global tree state map to ensure only one operation runs at a time.
Core implementation – refresh method with cancellation token :
class TreeNode {
...
async refresh(
expandedPaths: string[] = this.getAllExpandedNodePath(), // get expanded node paths
token?: CancellationToken,
) {
const childrens = (await this._tree.resolveChildren(this)) || [];
if (token?.isCancellationRequested) {
return;
}
while ((forceLoadPath = expandedPaths.shift())) {
const child = childrens?.find((child) => child.path === forceLoadPath);
if (CompositeTreeNode.is(child)) {
await child.resolveChildrens(); // load child nodes
if (token?.isCancellationRequested) {
return;
}
await child.refresh(expandedPaths, token) // recurse
if (token?.isCancellationRequested) {
return;
}
}
}
if (forceLoadPath) {
expandedPaths.unshift(forceLoadPath);
this.expandBranch(this, true); // merge latest FlattenBranch data
} else if (CompositeTreeNode.isRoot(this)) {
const expandedChilds: CompositeTreeNode[] = [];
const flatTree = new Array(childrens.length);
this._children = Array(childrens.length);
for (let i = 0; i < childrens.length; i++) {
const child = childrens[i];
this._children[i] = child;
if (CompositeTreeNode.is(child) && child.expanded) {
expandedChilds.push(child);
}
}
this._children.sort(this._tree.sortComparator || CompositeTreeNode.defaultSortComparator);
for (let i = 0; i < childrens.length; i++) {
flatTree[i] = this._children[i].id;
}
this._branchSize = flatTree.length;
this.setFlattenedBranch(flatTree, true);
for (let i = 0; i < expandedChilds.length; i++) {
const child = expandedChilds[i];
child.expandBranch(child, true);
}
// notify view update
}
}
...
}Global tree state management :
class TreeNode {
...
public static pathToGlobalTreeState: Map
= new Map();
public static getGlobalTreeState(path: string) {
const root = path.split(Path.separator).slice(0, 2).join(Path.separator);
let state = TreeNode.pathToGlobalTreeState.get(root);
if (!state) {
state = {
isExpanding: false,
isLoadingPath: false,
isRefreshing: false,
refreshCancelToken: new CancellationTokenSource(),
loadPathCancelToken: new CancellationTokenSource(),
};
}
return state;
}
public static setGlobalTreeState(path: string, updateState: IOptionalGlobalTreeState) {
const root = path.split(Path.separator).slice(0, 2).join(Path.separator);
let state = TreeNode.pathToGlobalTreeState.get(root);
if (!state) {
state = {
isExpanding: false,
isLoadingPath: false,
isRefreshing: false,
refreshCancelToken: new CancellationTokenSource(),
loadPathCancelToken: new CancellationTokenSource(),
};
}
state = { ...state, ...updateState };
TreeNode.pathToGlobalTreeState.set(root, state);
return state;
}
...
}Example of cancelling non‑core operations when expanding a node :
class TreeNode {
...
public async setExpanded(ensureVisible = true, quiet = false, isOwner = true, token?: CancellationToken) {
const state = TreeNode.getGlobalTreeState(this.path);
// cancel possible ongoing locate‑node task
state.loadPathCancelToken.cancel();
// cancel possible ongoing refresh task
state.refreshCancelToken.cancel();
TreeNode.setGlobalTreeState(this.path, { isExpanding: true });
this.isExpanded = true;
if (this._children === null) {
await this.hardReloadChildren(token);
}
...
}
...
}After applying these changes, the file‑tree remains stable and responsive even under rapid file modifications, providing a smooth user experience without rendering glitches.
DaTaobao Tech
Official account of DaTaobao Technology
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.