Skip to content

UncontrolledTreeEnvironment builds its item map with a spread inside reduce, which is quadratic #441

Description

@vivek-perforce

Describe the bug

UncontrolledTreeEnvironment builds the item map like this:

items
  .map(item => ({ [item.index]: item }))
  .reduce((a, b) => ({ ...a, ...b }), {})

Each step copies the whole accumulator, so loading N items does around N²/2 property writes instead of N. It also grows every intermediate object one property at a time, which puts them into dictionary mode and creates a lot of garbage.

The same code is in two places:

  • packages/core/src/uncontrolledEnvironment/UncontrolledTreeEnvironment.tsx lines 69-72, in the onDidChangeTreeData handler
  • same file, lines 223-226, in onMissingItems

This is fine on small trees, but gets very bad on large ones.

To Reproduce

Use a TreeDataProvider whose getTreeItems returns a large number of items in one call. In our case the tree grew to roughly 40,000 items as data loaded in, and the page became unresponsive for minutes.

A Firefox profile of that load showed 277 seconds out of 279 spent in Object.assign under the reduce, plus about 59 seconds of major GC.

Replacing the reduce with a plain loop:

const itemMap = {};
for (const item of items) {
  itemMap[item.index] = item;
}

brought the same load down to 134 ms.

Expected behavior

The item map is built in a single pass, without copying the accumulator for every item.

Additional context

  • macOS, Firefox
  • 2.6.0, and the same code is in 2.6.1

Smaller, related: writeItems (line 43) does { ...oldItems, ...newItems }, which copies the whole map on every call. That is cheap when items arrive in a few large batches, but it means fetching in many small batches costs more than it looks like it should.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions