Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function benchmarkFileListToTreeStages(
stageTimingsMs.buildFolderNodes = buildFolderNodesMs;

const [tree, hashTreeKeysMs] = timeStage(() =>
hashFileListToTreeKeys(state.tree, state.treeEntries, rootId)
hashFileListToTreeKeys(state.tree)
);
stageTimingsMs.hashTreeKeys = hashTreeKeysMs;

Expand Down
20 changes: 16 additions & 4 deletions packages/trees/src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ import { generateSyncDataLoaderFromTreeData } from '../loader/sync';
import type { SVGSpriteNames } from '../sprite';
import type { FileTreeNode } from '../types';
import { computeNewFilesAfterDrop } from '../utils/computeNewFilesAfterDrop';
import { fileListToTree } from '../utils/fileListToTree';
import {
fileListToTree,
getFileListToTreePathToIdMap,
} from '../utils/fileListToTree';
import { getGitStatusSignature } from '../utils/getGitStatusSignature';
import { getSelectionPath } from '../utils/getSelectionPath';
import { renameFileTreePaths } from '../utils/renameFileTreePaths';
Expand Down Expand Up @@ -183,11 +186,20 @@ export function Root({
[benchmarkInstrumentation, files, sortComparator]
);

// Build the hot path->id map with a direct for-in scan and answer id->path
// lookups straight from treeData so we do not duplicate the whole tree into a
// second Map on every fresh mount.
// Reuse the path->id map precomputed during fileListToTree hashing when
// available so Root does not need to rescan the full tree object.
const pathToId = useMemo(() => {
return withBenchmarkPhase(benchmarkInstrumentation, 'root.pathToId', () => {
const precomputed = getFileListToTreePathToIdMap(treeData);
if (precomputed != null) {
setBenchmarkCounter(
benchmarkInstrumentation,
'workload.pathToIdEntries',
precomputed.size
);
return precomputed;
}

const next = new Map<string, string>();
for (const id in treeData) {
const node = treeData[id];
Expand Down
88 changes: 48 additions & 40 deletions packages/trees/src/features/tree/feature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* oxlint-disable typescript-eslint/no-unsafe-return, typescript-eslint/strict-boolean-expressions */
import type { FeatureImplementation } from '../../core/types/core';
import { logWarning } from '../../core/utilities/errors';
import { makeStateUpdater, poll } from '../../core/utils';
import type { ItemMeta } from './types';

Expand Down Expand Up @@ -31,55 +30,64 @@ export const treeFeature: FeatureImplementation<any> = {
const { expandedItems } = tree.getState();
const flatItems: ItemMeta[] = [];
const expandedItemsSet = new Set(expandedItems); // TODO support setting state expandedItems as set instead of array
const lineageSet = new Set([rootItemId]);
const lineageStack = [rootItemId];
const rootChildren = tree.retrieveChildrenIds(rootItemId) ?? [];

// Walks visible tree nodes while reusing a single lineage stack/set for
// circular-reference detection to avoid per-node path array allocations.
const recursiveAdd = (
itemId: string,
parentId: string,
level: number,
setSize: number,
posInSet: number
) => {
if (lineageSet.has(itemId)) {
logWarning(
`Circular reference for ${[...lineageStack, itemId].join('.')}`
);
return;
}
// Iterative pre-order traversal avoids per-node recursive call overhead
// on very large fully-expanded trees.
const stack: Array<{
itemId: string;
parentId: string;
level: number;
setSize: number;
posInSet: number;
}> = [];

flatItems.push({
for (
let childIndex = rootChildren.length - 1;
childIndex >= 0;
childIndex--
) {
const itemId = rootChildren[childIndex];
stack.push({
itemId,
level,
parentId: rootItemId,
level: 0,
setSize: rootChildren.length,
posInSet: childIndex,
});
}

while (stack.length > 0) {
const current = stack.pop()!;

flatItems.push({
itemId: current.itemId,
level: current.level,
index: flatItems.length,
parentId,
setSize,
posInSet,
parentId: current.parentId,
setSize: current.setSize,
posInSet: current.posInSet,
});

if (!expandedItemsSet.has(itemId)) {
return;
if (!expandedItemsSet.has(current.itemId)) {
continue;
}

lineageSet.add(itemId);
lineageStack.push(itemId);

const children = tree.retrieveChildrenIds(itemId) ?? [];
for (let childIndex = 0; childIndex < children.length; childIndex++) {
const children = tree.retrieveChildrenIds(current.itemId) ?? [];
for (
let childIndex = children.length - 1;
childIndex >= 0;
childIndex--
) {
const childId = children[childIndex];
recursiveAdd(childId, itemId, level + 1, children.length, childIndex);
stack.push({
itemId: childId,
parentId: current.itemId,
level: current.level + 1,
setSize: children.length,
posInSet: childIndex,
});
}

lineageStack.pop();
lineageSet.delete(itemId);
};

const children = tree.retrieveChildrenIds(rootItemId) ?? [];
for (let childIndex = 0; childIndex < children.length; childIndex++) {
const itemId = children[childIndex];
recursiveAdd(itemId, rootItemId, 0, children.length, childIndex);
}

return flatItems;
Expand Down
9 changes: 3 additions & 6 deletions packages/trees/src/loader/lazy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FLATTENED_PREFIX } from '../constants';
import type { FileTreeNode } from '../types';
import { createIdMaps } from '../utils/createIdMaps';
import { createLoaderUtils } from '../utils/createLoaderUtils';
import {
forEachFolderInNormalizedPath,
Expand Down Expand Up @@ -139,7 +138,6 @@ export function generateLazyDataLoader(
flattenedKeys.add(`${FLATTENED_PREFIX}${flattenedEndpoint}`);
}

const { getIdForKey, getKeyForId } = createIdMaps(rootId);
const allKeys = new Set<string>([rootId]);
for (const path of sortedPaths) {
allKeys.add(path);
Expand All @@ -150,11 +148,10 @@ export function generateLazyDataLoader(
for (const flattenedKey of flattenedKeys) {
allKeys.add(flattenedKey);
}
for (const key of Array.from(allKeys).sort()) {
getIdForKey(key);
}

const mapKey = (key: string) => getIdForKey(key);
const mapKey = (key: string): string => key;
const getKeyForId = (id: string): string | undefined =>
allKeys.has(id) ? id : undefined;

// Find the start of the flattened chain that ends at endPath
const findFlattenedChainStart = (endPath: string): string => {
Expand Down
Loading
Loading