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
171 changes: 154 additions & 17 deletions packages/path-store/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ export function preparePresortedInput(
return {
paths: presortedPaths,
presortedPaths,
presortedPathsContainDirectories: presortedPaths.some((path) =>
path.endsWith('/')
),
};
}

Expand All @@ -165,6 +168,16 @@ export function getPreparedInputPresortedPaths(
: null;
}

export function getPreparedInputPresortedPathsContainDirectories(
preparedInput: import('./public-types').PathStorePreparedInput
): boolean | null {
const internalPreparedInput = preparedInput as Partial<InternalPreparedInput>;
return typeof internalPreparedInput.presortedPathsContainDirectories ===
'boolean'
? internalPreparedInput.presortedPathsContainDirectories
: null;
}

export function preparePathEntries(
paths: readonly string[],
options: PathStoreOptions = {}
Expand Down Expand Up @@ -229,11 +242,19 @@ export class PathStoreBuilder {
return this;
}

public appendPresortedPaths(paths: readonly string[]): this {
public appendPresortedPaths(
paths: readonly string[],
containsDirectories: boolean | null = null
): this {
withBenchmarkPhase(
this.instrumentation,
'store.builder.appendPresortedPaths',
() => {
if (containsDirectories === false) {
this.appendPresortedFilePaths(paths);
return;
}

let previousPath: string | null = null;
let currentDepth = 0;
const nodes = this.nodes;
Expand Down Expand Up @@ -440,6 +461,138 @@ export class PathStoreBuilder {
return this;
}

// File-only presorted input can skip all explicit-directory handling and the
// trailing-slash checks in the hottest builder loop.
private appendPresortedFilePaths(paths: readonly string[]): void {
let previousPath: string | null = null;
let currentDepth = 0;
const nodes = this.nodes;
const segmentTable = this.segmentTable;
const idByValue = segmentTable.idByValue;
const valueById = segmentTable.valueById;
const sortKeyById = segmentTable.sortKeyById;
const dirStack = this.directoryStack;
let stackTop = 0;
let cachedDirPrefix = '';
let cachedDirDepth = 0;

for (const path of paths) {
if (previousPath === path) {
throw new Error(`Duplicate path: "${path}"`);
}

const endIndex = path.length;
let sharedDirectoryDepth = 0;
let unsharedSegmentStart = 0;

if (previousPath != null) {
if (
cachedDirPrefix.length > 0 &&
path.length > cachedDirPrefix.length &&
path.startsWith(cachedDirPrefix)
) {
sharedDirectoryDepth = cachedDirDepth;
unsharedSegmentStart = cachedDirPrefix.length;
} else {
const compareLength = Math.min(endIndex, previousPath.length);
for (let ci = 0; ci < compareLength; ci++) {
const cc = path.charCodeAt(ci);
if (cc !== previousPath.charCodeAt(ci)) {
break;
}
if (cc === 47) {
sharedDirectoryDepth++;
unsharedSegmentStart = ci + 1;
}
}
}
}

stackTop = sharedDirectoryDepth;
currentDepth = sharedDirectoryDepth;

let segmentStart = unsharedSegmentStart;
let slashPos = path.indexOf('/', segmentStart);
while (slashPos >= 0) {
const parentId = dirStack[stackTop];
if (parentId === undefined) {
throw new Error(
'Directory stack underflow while building the path store'
);
}

currentDepth++;
const dirSeg = path.slice(segmentStart, slashPos);
let dirNameId = idByValue[dirSeg];
if (dirNameId === undefined) {
dirNameId = valueById.length;
idByValue[dirSeg] = dirNameId;
valueById.push(dirSeg);
sortKeyById.push(undefined);
}
const nodeId = nodes.length;
nodes.push({
depth: currentDepth,
flags: 0,
id: nodeId,
kind: PATH_STORE_NODE_KIND_DIRECTORY,
nameId: dirNameId,
parentId,
pathCache: null,
pathCacheVersion: 0,
subtreeNodeCount: 1,
visibleSubtreeCount: 1,
});
stackTop++;
dirStack[stackTop] = nodeId;
segmentStart = slashPos + 1;
slashPos = path.indexOf('/', segmentStart);
}

const parentId = dirStack[stackTop];
if (parentId === undefined) {
throw new Error(`Unable to resolve file parent for "${path}"`);
}

const fileSeg = path.slice(segmentStart);
let fileNameId = idByValue[fileSeg];
if (fileNameId === undefined) {
fileNameId = valueById.length;
idByValue[fileSeg] = fileNameId;
valueById.push(fileSeg);
sortKeyById.push(undefined);
}
const nodeId = nodes.length;
nodes.push({
depth: currentDepth + 1,
flags: 0,
id: nodeId,
kind: PATH_STORE_NODE_KIND_FILE,
nameId: fileNameId,
parentId,
pathCache: null,
pathCacheVersion: -1,
subtreeNodeCount: 1,
visibleSubtreeCount: 1,
});

if (segmentStart !== cachedDirPrefix.length) {
cachedDirPrefix = path.substring(0, segmentStart);
cachedDirDepth = currentDepth;
}

previousPath = path;
}

dirStack.length = stackTop + 1;

if (previousPath != null) {
this.lastPreparedPath = parseInputPath(previousPath);
}

this.hasDeferredDirectoryIndexes = true;
}

public finish(): PathStoreSnapshot {
if (this.hasDeferredDirectoryIndexes) {
withBenchmarkPhase(
Expand Down Expand Up @@ -822,22 +975,6 @@ export class PathStoreBuilder {
parentNode.visibleSubtreeCount += node.visibleSubtreeCount;
}
}

// Eagerly build name-id lookup maps so later path lookups (e.g.,
// initializeExpandedPaths) don't pay the lazy-rebuild cost per directory.
// This is O(n) total and cache-friendlier than building maps on-demand
// during random tree walks.
for (const dirIndex of directories.values()) {
if (dirIndex.childIdByNameId == null && dirIndex.childIds.length > 0) {
const map = new Map<number, number>();
const childIds = dirIndex.childIds;
for (let ci = 0; ci < childIds.length; ci++) {
const childId = childIds[ci];
map.set(nodes[childId].nameId, childId);
}
dirIndex.childIdByNameId = map;
}
}
}

// Builds directory-child indexes in the same layout as buildPresortedFinish
Expand Down
2 changes: 2 additions & 0 deletions packages/path-store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export type {
PathStoreOperation,
PathStoreOptions,
PathStorePathComparator,
PathStorePathInfo,
PathStorePreparedInput,
PathStoreRemoveOptions,
PathStoreVisibleRow,
PathStoreVisibleTreeProjection,
PathStoreVisibleTreeProjectionData,
PathStoreVisibleTreeProjectionRow,
} from './public-types';
export type {
Expand Down
1 change: 1 addition & 0 deletions packages/path-store/src/internal-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface PreparedPath {
export type InternalPreparedInput = PathStorePreparedInput & {
readonly preparedPaths?: readonly PreparedPath[];
readonly presortedPaths?: readonly string[];
readonly presortedPathsContainDirectories?: boolean;
};

export interface LookupPath {
Expand Down
Loading
Loading