Skip to content

Commit 2513a2d

Browse files
authored
Use binary search in file system cache (#50163)
* Use binary search in file system cache Previously, we were linear searching a linear number of times, resulting in too many `toLowerCaseFileName` calls on Windows. * Use SortedArray types for clarity * Use insertSorted after making it return a flat * Drop redundant undefined * Correct copy-paste error
1 parent af90e70 commit 2513a2d

File tree

2 files changed

+62
-28
lines changed

2 files changed

+62
-28
lines changed

src/compiler/core.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -778,19 +778,24 @@ namespace ts {
778778
return [] as any as SortedArray<T>; // TODO: GH#19873
779779
}
780780

781-
export function insertSorted<T>(array: SortedArray<T>, insert: T, compare: Comparer<T>, allowDuplicates?: boolean): void {
781+
export function insertSorted<T>(array: SortedArray<T>, insert: T, compare: Comparer<T>, allowDuplicates?: boolean): boolean {
782782
if (array.length === 0) {
783783
array.push(insert);
784-
return;
784+
return true;
785785
}
786786

787787
const insertIndex = binarySearch(array, insert, identity, compare);
788788
if (insertIndex < 0) {
789789
array.splice(~insertIndex, 0, insert);
790+
return true;
790791
}
791-
else if (allowDuplicates) {
792+
793+
if (allowDuplicates) {
792794
array.splice(insertIndex, 0, insert);
795+
return true;
793796
}
797+
798+
return false;
794799
}
795800

796801
export function sortAndDeduplicate<T>(array: readonly string[]): SortedReadonlyArray<string>;

src/compiler/watchUtilities.ts

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,20 @@ namespace ts {
3434
clearCache(): void;
3535
}
3636

37+
type Canonicalized = string & { __canonicalized: void };
38+
3739
interface MutableFileSystemEntries {
3840
readonly files: string[];
3941
readonly directories: string[];
42+
sortedAndCanonicalizedFiles?: SortedArray<Canonicalized>
43+
sortedAndCanonicalizedDirectories?: SortedArray<Canonicalized>
44+
}
45+
46+
interface SortedAndCanonicalizedMutableFileSystemEntries {
47+
readonly files: string[];
48+
readonly directories: string[];
49+
readonly sortedAndCanonicalizedFiles: SortedArray<Canonicalized>
50+
readonly sortedAndCanonicalizedDirectories: SortedArray<Canonicalized>
4051
}
4152

4253
export function createCachedDirectoryStructureHost(host: DirectoryStructureHost, currentDirectory: string, useCaseSensitiveFileNames: boolean): CachedDirectoryStructureHost | undefined {
@@ -45,7 +56,7 @@ namespace ts {
4556
}
4657

4758
const cachedReadDirectoryResult = new Map<string, MutableFileSystemEntries | false>();
48-
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
59+
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames) as ((name: string) => Canonicalized);
4960
return {
5061
useCaseSensitiveFileNames,
5162
fileExists,
@@ -70,7 +81,17 @@ namespace ts {
7081
}
7182

7283
function getCachedFileSystemEntriesForBaseDir(path: Path) {
73-
return getCachedFileSystemEntries(getDirectoryPath(path));
84+
const entries = getCachedFileSystemEntries(getDirectoryPath(path));
85+
if (!entries) {
86+
return entries;
87+
}
88+
89+
// If we're looking for the base directory, we're definitely going to search the entries
90+
if (!entries.sortedAndCanonicalizedFiles) {
91+
entries.sortedAndCanonicalizedFiles = entries.files.map(getCanonicalFileName).sort() as SortedArray<Canonicalized>;
92+
entries.sortedAndCanonicalizedDirectories = entries.directories.map(getCanonicalFileName).sort() as SortedArray<Canonicalized>;
93+
}
94+
return entries as SortedAndCanonicalizedMutableFileSystemEntries;
7495
}
7596

7697
function getBaseNameOfFileName(fileName: string) {
@@ -120,23 +141,10 @@ namespace ts {
120141
}
121142
}
122143

123-
function fileNameEqual(name1: string, name2: string) {
124-
return getCanonicalFileName(name1) === getCanonicalFileName(name2);
125-
}
126-
127-
function hasEntry(entries: readonly string[], name: string) {
128-
return some(entries, file => fileNameEqual(file, name));
129-
}
130-
131-
function updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) {
132-
if (hasEntry(entries, baseName)) {
133-
if (!isValid) {
134-
return filterMutate(entries, entry => !fileNameEqual(entry, baseName));
135-
}
136-
}
137-
else if (isValid) {
138-
return entries.push(baseName);
139-
}
144+
function hasEntry(entries: SortedReadonlyArray<Canonicalized>, name: Canonicalized) {
145+
// Case-sensitive comparison since already canonicalized
146+
const index = binarySearch(entries, name, identity, compareStringsCaseSensitive);
147+
return index >= 0;
140148
}
141149

142150
function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void {
@@ -151,7 +159,7 @@ namespace ts {
151159
function fileExists(fileName: string): boolean {
152160
const path = toPath(fileName);
153161
const result = getCachedFileSystemEntriesForBaseDir(path);
154-
return result && hasEntry(result.files, getBaseNameOfFileName(fileName)) ||
162+
return result && hasEntry(result.sortedAndCanonicalizedFiles, getCanonicalFileName(getBaseNameOfFileName(fileName))) ||
155163
host.fileExists(fileName);
156164
}
157165

@@ -163,9 +171,14 @@ namespace ts {
163171
function createDirectory(dirPath: string) {
164172
const path = toPath(dirPath);
165173
const result = getCachedFileSystemEntriesForBaseDir(path);
166-
const baseFileName = getBaseNameOfFileName(dirPath);
167174
if (result) {
168-
updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true);
175+
const baseName = getBaseNameOfFileName(dirPath);
176+
const canonicalizedBaseName = getCanonicalFileName(baseName);
177+
const canonicalizedDirectories = result.sortedAndCanonicalizedDirectories;
178+
// Case-sensitive comparison since already canonicalized
179+
if (insertSorted(canonicalizedDirectories, canonicalizedBaseName, compareStringsCaseSensitive)) {
180+
result.directories.push(baseName);
181+
}
169182
}
170183
host.createDirectory!(dirPath);
171184
}
@@ -242,7 +255,7 @@ namespace ts {
242255
fileExists: host.fileExists(fileOrDirectoryPath),
243256
directoryExists: host.directoryExists(fileOrDirectoryPath)
244257
};
245-
if (fsQueryResult.directoryExists || hasEntry(parentResult.directories, baseName)) {
258+
if (fsQueryResult.directoryExists || hasEntry(parentResult.sortedAndCanonicalizedDirectories, getCanonicalFileName(baseName))) {
246259
// Folder added or removed, clear the cache instead of updating the folder and its structure
247260
clearCache();
248261
}
@@ -265,8 +278,24 @@ namespace ts {
265278
}
266279
}
267280

268-
function updateFilesOfFileSystemEntry(parentResult: MutableFileSystemEntries, baseName: string, fileExists: boolean) {
269-
updateFileSystemEntry(parentResult.files, baseName, fileExists);
281+
function updateFilesOfFileSystemEntry(parentResult: SortedAndCanonicalizedMutableFileSystemEntries, baseName: string, fileExists: boolean): void {
282+
const canonicalizedFiles = parentResult.sortedAndCanonicalizedFiles;
283+
const canonicalizedBaseName = getCanonicalFileName(baseName);
284+
if (fileExists) {
285+
// Case-sensitive comparison since already canonicalized
286+
if (insertSorted(canonicalizedFiles, canonicalizedBaseName, compareStringsCaseSensitive)) {
287+
parentResult.files.push(baseName);
288+
}
289+
}
290+
else {
291+
// Case-sensitive comparison since already canonicalized
292+
const sortedIndex = binarySearch(canonicalizedFiles, canonicalizedBaseName, identity, compareStringsCaseSensitive);
293+
if (sortedIndex >= 0) {
294+
canonicalizedFiles.splice(sortedIndex, 1);
295+
const unsortedIndex = parentResult.files.findIndex(entry => getCanonicalFileName(entry) === canonicalizedBaseName);
296+
parentResult.files.splice(unsortedIndex, 1);
297+
}
298+
}
270299
}
271300

272301
function clearCache() {

0 commit comments

Comments
 (0)