Skip to content

Commit 9549928

Browse files
authored
Make export-module and reference maps invertible (#44402)
* Make export-module and reference maps invertible Right now, we're enumerating all the entries to find out which keys map to a corresponding value. By maintaining a two-way map, we can convert this linear search into a map lookup and skip allocation of many, many iterator results. * Fix lint error * Add some explanatory comments * Rename, drop type parameters, and add readonly variant * Simplify member list * Fold non-exporting behavior into custom map type
1 parent 8dbb2cd commit 9549928

File tree

3 files changed

+183
-78
lines changed

3 files changed

+183
-78
lines changed

src/compiler/builder.ts

+50-35
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ namespace ts {
4545
/**
4646
* Newly computed visible to outside referencedSet
4747
*/
48-
currentAffectedFilesExportedModulesMap?: Readonly<BuilderState.ComputingExportedModulesMap> | undefined;
48+
currentAffectedFilesExportedModulesMap?: BuilderState.ReadonlyManyToManyPathMap | undefined;
4949
/**
5050
* True if the semantic diagnostics were copied from the old state
5151
*/
@@ -113,8 +113,10 @@ namespace ts {
113113
currentAffectedFilesSignatures: ESMap<Path, string> | undefined;
114114
/**
115115
* Newly computed visible to outside referencedSet
116+
* We need to store the updates separately in case the in-progress build is cancelled
117+
* and we need to roll back.
116118
*/
117-
currentAffectedFilesExportedModulesMap: BuilderState.ComputingExportedModulesMap | undefined;
119+
currentAffectedFilesExportedModulesMap: BuilderState.ManyToManyPathMap | undefined;
118120
/**
119121
* Already seen affected files
120122
*/
@@ -212,7 +214,7 @@ namespace ts {
212214
const copyLibFileDiagnostics = copyDeclarationFileDiagnostics && !compilerOptions.skipDefaultLibCheck === !oldCompilerOptions!.skipDefaultLibCheck;
213215
state.fileInfos.forEach((info, sourceFilePath) => {
214216
let oldInfo: Readonly<BuilderState.FileInfo> | undefined;
215-
let newReferences: BuilderState.ReferencedSet | undefined;
217+
let newReferences: ReadonlySet<Path> | undefined;
216218

217219
// if not using old state, every file is changed
218220
if (!useOldState ||
@@ -221,7 +223,7 @@ namespace ts {
221223
// versions dont match
222224
oldInfo.version !== info.version ||
223225
// Referenced files changed
224-
!hasSameKeys(newReferences = referencedMap && referencedMap.get(sourceFilePath), oldReferencedMap && oldReferencedMap.get(sourceFilePath)) ||
226+
!hasSameKeys(newReferences = referencedMap && referencedMap.getValues(sourceFilePath), oldReferencedMap && oldReferencedMap.getValues(sourceFilePath)) ||
225227
// Referenced file was deleted in the new program
226228
newReferences && forEachKey(newReferences, path => !state.fileInfos.has(path) && oldState!.fileInfos.has(path))) {
227229
// Register file as changed file and do not copy semantic diagnostics, since all changed files need to be re-evaluated
@@ -311,7 +313,7 @@ namespace ts {
311313
newState.affectedFilesIndex = state.affectedFilesIndex;
312314
newState.currentChangedFilePath = state.currentChangedFilePath;
313315
newState.currentAffectedFilesSignatures = state.currentAffectedFilesSignatures && new Map(state.currentAffectedFilesSignatures);
314-
newState.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap && new Map(state.currentAffectedFilesExportedModulesMap);
316+
newState.currentAffectedFilesExportedModulesMap = state.currentAffectedFilesExportedModulesMap?.clone();
315317
newState.seenAffectedFiles = state.seenAffectedFiles && new Set(state.seenAffectedFiles);
316318
newState.cleanedDiagnosticsOfLibFiles = state.cleanedDiagnosticsOfLibFiles;
317319
newState.semanticDiagnosticsFromOldState = state.semanticDiagnosticsFromOldState && new Set(state.semanticDiagnosticsFromOldState);
@@ -384,7 +386,7 @@ namespace ts {
384386
// Get next batch of affected files
385387
if (!state.currentAffectedFilesSignatures) state.currentAffectedFilesSignatures = new Map();
386388
if (state.exportedModulesMap) {
387-
if (!state.currentAffectedFilesExportedModulesMap) state.currentAffectedFilesExportedModulesMap = new Map();
389+
state.currentAffectedFilesExportedModulesMap ||= BuilderState.createManyToManyPathMap();
388390
}
389391
state.affectedFiles = BuilderState.getFilesAffectedBy(state, program, nextKey.value, cancellationToken, computeHash, state.currentAffectedFilesSignatures, state.currentAffectedFilesExportedModulesMap);
390392
state.currentChangedFilePath = nextKey.value;
@@ -465,7 +467,7 @@ namespace ts {
465467
* Handle the dts may change, so they need to be added to pending emit if dts emit is enabled,
466468
* Also we need to make sure signature is updated for these files
467469
*/
468-
function handleDtsMayChangeOf(state: BuilderProgramState, path: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash) {
470+
function handleDtsMayChangeOf(state: BuilderProgramState, path: Path, cancellationToken: CancellationToken | undefined, computeHash: BuilderState.ComputeHash): void {
469471
removeSemanticDiagnosticsOf(state, path);
470472

471473
if (!state.changedFilesSet.has(path)) {
@@ -544,36 +546,36 @@ namespace ts {
544546
}
545547

546548
Debug.assert(!!state.currentAffectedFilesExportedModulesMap);
549+
547550
const seenFileAndExportsOfFile = new Set<string>();
548551
// Go through exported modules from cache first
549552
// If exported modules has path, all files referencing file exported from are affected
550-
forEachEntry(state.currentAffectedFilesExportedModulesMap, (exportedModules, exportedFromPath) =>
551-
exportedModules &&
552-
exportedModules.has(affectedFile.resolvedPath) &&
553+
state.currentAffectedFilesExportedModulesMap.getKeys(affectedFile.resolvedPath)?.forEach(exportedFromPath =>
553554
forEachFilesReferencingPath(state, exportedFromPath, seenFileAndExportsOfFile, fn)
554555
);
555556

556557
// If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected
557-
forEachEntry(state.exportedModulesMap, (exportedModules, exportedFromPath) =>
558-
!state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it
559-
exportedModules.has(affectedFile.resolvedPath) &&
558+
state.exportedModulesMap.getKeys(affectedFile.resolvedPath)?.forEach(exportedFromPath =>
559+
// If the cache had an updated value, skip
560+
!state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) &&
561+
!state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) &&
560562
forEachFilesReferencingPath(state, exportedFromPath, seenFileAndExportsOfFile, fn)
561563
);
562564
}
563565

564566
/**
565567
* Iterate on files referencing referencedPath
566568
*/
567-
function forEachFilesReferencingPath(state: BuilderProgramState, referencedPath: Path, seenFileAndExportsOfFile: Set<string>, fn: (state: BuilderProgramState, filePath: Path) => void) {
568-
forEachEntry(state.referencedMap!, (referencesInFile, filePath) =>
569-
referencesInFile.has(referencedPath) && forEachFileAndExportsOfFile(state, filePath, seenFileAndExportsOfFile, fn)
569+
function forEachFilesReferencingPath(state: BuilderProgramState, referencedPath: Path, seenFileAndExportsOfFile: Set<string>, fn: (state: BuilderProgramState, filePath: Path) => void): void {
570+
state.referencedMap!.getKeys(referencedPath)?.forEach(filePath =>
571+
forEachFileAndExportsOfFile(state, filePath, seenFileAndExportsOfFile, fn)
570572
);
571573
}
572574

573575
/**
574576
* fn on file and iterate on anything that exports this file
575577
*/
576-
function forEachFileAndExportsOfFile(state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: Set<string>, fn: (state: BuilderProgramState, filePath: Path) => void) {
578+
function forEachFileAndExportsOfFile(state: BuilderProgramState, filePath: Path, seenFileAndExportsOfFile: Set<string>, fn: (state: BuilderProgramState, filePath: Path) => void): void {
577579
if (!tryAddToSet(seenFileAndExportsOfFile, filePath)) {
578580
return;
579581
}
@@ -583,23 +585,20 @@ namespace ts {
583585
Debug.assert(!!state.currentAffectedFilesExportedModulesMap);
584586
// Go through exported modules from cache first
585587
// If exported modules has path, all files referencing file exported from are affected
586-
forEachEntry(state.currentAffectedFilesExportedModulesMap, (exportedModules, exportedFromPath) =>
587-
exportedModules &&
588-
exportedModules.has(filePath) &&
588+
state.currentAffectedFilesExportedModulesMap.getKeys(filePath)?.forEach(exportedFromPath =>
589589
forEachFileAndExportsOfFile(state, exportedFromPath, seenFileAndExportsOfFile, fn)
590590
);
591591

592592
// If exported from path is not from cache and exported modules has path, all files referencing file exported from are affected
593-
forEachEntry(state.exportedModulesMap!, (exportedModules, exportedFromPath) =>
594-
!state.currentAffectedFilesExportedModulesMap!.has(exportedFromPath) && // If we already iterated this through cache, ignore it
595-
exportedModules.has(filePath) &&
593+
state.exportedModulesMap!.getKeys(filePath)?.forEach(exportedFromPath =>
594+
// If the cache had an updated value, skip
595+
!state.currentAffectedFilesExportedModulesMap!.hasKey(exportedFromPath) &&
596+
!state.currentAffectedFilesExportedModulesMap!.deletedKeys()?.has(exportedFromPath) &&
596597
forEachFileAndExportsOfFile(state, exportedFromPath, seenFileAndExportsOfFile, fn)
597598
);
598599

599600
// Remove diagnostics of files that import this file (without going to exports of referencing files)
600-
601-
forEachEntry(state.referencedMap!, (referencesInFile, referencingFilePath) =>
602-
referencesInFile.has(filePath) &&
601+
state.referencedMap!.getKeys(filePath)?.forEach(referencingFilePath =>
603602
!seenFileAndExportsOfFile.has(referencingFilePath) && // Not already removed diagnostic file
604603
fn(state, referencingFilePath) // Dont add to seen since this is not yet done with the export removal
605604
);
@@ -756,18 +755,26 @@ namespace ts {
756755
if (state.referencedMap) {
757756
referencedMap = arrayFrom(state.referencedMap.keys()).sort(compareStringsCaseSensitive).map(key => [
758757
toFileId(key),
759-
toFileIdListId(state.referencedMap!.get(key)!)
758+
toFileIdListId(state.referencedMap!.getValues(key)!)
760759
]);
761760
}
762761

763762
let exportedModulesMap: ProgramBuildInfoReferencedMap | undefined;
764763
if (state.exportedModulesMap) {
765764
exportedModulesMap = mapDefined(arrayFrom(state.exportedModulesMap.keys()).sort(compareStringsCaseSensitive), key => {
766-
const newValue = state.currentAffectedFilesExportedModulesMap && state.currentAffectedFilesExportedModulesMap.get(key);
765+
if (state.currentAffectedFilesExportedModulesMap) {
766+
if (state.currentAffectedFilesExportedModulesMap.deletedKeys()?.has(key)) {
767+
return undefined;
768+
}
769+
770+
const newValue = state.currentAffectedFilesExportedModulesMap.getValues(key);
771+
if (newValue) {
772+
return [toFileId(key), toFileIdListId(newValue)];
773+
}
774+
}
775+
767776
// Not in temporary cache, use existing value
768-
if (newValue === undefined) return [toFileId(key), toFileIdListId(state.exportedModulesMap!.get(key)!)];
769-
// Value in cache and has updated value map, use that
770-
else if (newValue) return [toFileId(key), toFileIdListId(newValue)];
777+
return [toFileId(key), toFileIdListId(state.exportedModulesMap!.getValues(key)!)];
771778
});
772779
}
773780

@@ -1251,8 +1258,8 @@ namespace ts {
12511258
const state: ReusableBuilderProgramState = {
12521259
fileInfos,
12531260
compilerOptions: program.options ? convertToOptionsWithAbsolutePaths(program.options, toAbsolutePath) : {},
1254-
referencedMap: toMapOfReferencedSet(program.referencedMap),
1255-
exportedModulesMap: toMapOfReferencedSet(program.exportedModulesMap),
1261+
referencedMap: toManyToManyPathMap(program.referencedMap),
1262+
exportedModulesMap: toManyToManyPathMap(program.exportedModulesMap),
12561263
semanticDiagnosticsPerFile: program.semanticDiagnosticsPerFile && arrayToMap(program.semanticDiagnosticsPerFile, value => toFilePath(isNumber(value) ? value : value[0]), value => isNumber(value) ? emptyArray : value[1]),
12571264
hasReusableDiagnostic: true,
12581265
affectedFilesPendingEmit: map(program.affectedFilesPendingEmit, value => toFilePath(value[0])),
@@ -1300,8 +1307,16 @@ namespace ts {
13001307
return filePathsSetList![fileIdsListId - 1];
13011308
}
13021309

1303-
function toMapOfReferencedSet(referenceMap: ProgramBuildInfoReferencedMap | undefined): ReadonlyESMap<Path, BuilderState.ReferencedSet> | undefined {
1304-
return referenceMap && arrayToMap(referenceMap, value => toFilePath(value[0]), value => toFilePathsSet(value[1]));
1310+
function toManyToManyPathMap(referenceMap: ProgramBuildInfoReferencedMap | undefined): BuilderState.ManyToManyPathMap | undefined {
1311+
if (!referenceMap) {
1312+
return undefined;
1313+
}
1314+
1315+
const map = BuilderState.createManyToManyPathMap();
1316+
referenceMap.forEach(([fileId, fileIdListId]) =>
1317+
map.set(toFilePath(fileId), toFilePathsSet(fileIdListId))
1318+
);
1319+
return map;
13051320
}
13061321
}
13071322

0 commit comments

Comments
 (0)