packages/ui/explorer.md
This document describes the overall behavior of the new Explorer component and the logic for:
Test Only filterCheck Notes for a brief summary about the old and the new logic.
The explorer will not use the idsMap and filesMap directly from the ws-client state to render the tree. It will use new types to represent the tree in the ui, and a new logic to handle the tree list in the DOM:
ws-client state will be mapped here with tree structure.nodes to build it.Any operation in the explorer using queueMicrotask to avoid blocking the main thread, and any operation on list/map using generators.
The explorer logic splits the actions in three main parts:
Whereas collecting and searching are complex operations, expanding/collapsing nodes is a simple operation. Why?:
ws-client messages from the server, and the nodes in the ui must be updated to reflect the state.expanded property (expanding all nodes requires full search).Old ws-client logic was traversing the full tree to update all the nodes in the ui on every onTaskUpdate callback (was using reactive idsMap in the ws-client state).
The new logic will traverse only the task files in the task result provided in the onTaskUpdate callback, that's a huge improvement in performance.
The main change in the new logic is about using requestAnimationFrame to collect ui updates every 100ms, collecting all changes received in onTaskUpdate callback. On every loop, the uiEntries will be recreated with collected changes, the virtual scroller will handle the updates properly.
The logic is implemented in collect function and the requestAnimationFrame loop configured in the tree class, runCollect function.
Search and filtering are quite simple, we only need to apply some logic to the task name, mode and result state. The complexity lies in filtering the nodes of the entire tree. We need to traverse the tree several times:
visitNodes function in the filter module.filterParents in the filter module.The main logic is the expandNode function in the filter module, will apply previous logic.
The search logic can be found in filter module.
This is the cheapest operation in the explorer, it only requires traversing the nodes in the ui and updating the expanded property:
expanded to false, then filter the uiEntries by file type.expanded to false for the node and all its children, and replace the child in the uiEntries with the new collapsed one, removing its children from uiEntries.The actions can be found in the tree class, collapseAllNodes and collapseNode methods, and the logic in the collapse.ts module.
This is also an affordable operation in the explorer, it only requires traversing the nodes in the ui and updating the expanded property:
expanded to true, then rebuild the uiEntries using filterAll in the search module.expanded to true, then filter its children using filterNode in the search module, and rebuild uiEntries replacing the current node in the ui tree with the new node and its filtered children.The actions can be found in the tree class, expandAllNodes and expandNode methods, and the logic in the expand.ts module.
The previous tree list approach was using a nested structure to map the tree rendering the full tree in the DOM. It was using the entries from the WebSocket client state (idsMap and filesMap), using Vue reactive for both maps. Since we updated Vue dependency to latest v3.4.27, every message received from the server was updating the corresponding entries in the maps. The tree list was being updated accordingly, firing a lot of patch updates in the vue components in the recursive tree, which was causing performance issues.
The new Explorer is using a flat structure to represent the tree via virtual scroller (vue-virtual-scroller). This new structure is easier to handle and manipulate, and it's also a performance improvement since the virtual scroller will update only a few nodes in the ui and not the full tree in the DOM.
It is using a new approach to handle the tree list, now we have a separated vue shallow ref for entries in the ui (uiEntries in composables/explorer/state.ts), and the WebSocket state using vue shallow ref for both, idsMap and filesMap, while keeping the state itself with Vue reactive.
Now we are able to update the tree list only when the entries are updated and not when the WebSocket state is updated, which is a huge performance improvement.
Some numbers running test/core with Vitest UI (162 files with 3 workspaces: 5100+ tests) in a i7-12700H laptop:
Expanding/collapsing nodes or searching with tree list approach is blocking the main thread, while with the new explorer, it's not blocking the main thread anymore, it is just instant.
Whereas the new explorer will not be affected by the number of tests, the number of tests affected the tree list approach, the more tests, the slower the ui was.
Anyway, on really huge projects, both approaches will have performance issues, but the new explorer will be much better than the tree list approach (vue-virtual-scroller should be fine using ~500_000 entries, check the docs).