Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor export map cache to not store transient symbols #44816

Merged
merged 10 commits into from
Jul 6, 2021

Conversation

andrewbranch
Copy link
Member

Follow-up to #44176 in a way, but fixes a memory leak that has been present for much longer. Previously, the cache stored exported symbols as-is, including transient ones, which can store a Type, which stores the TypeChecker it came from. We were never retaining more than one stale checker at a time, and were clearing it frequently as the program changed shape and the user moved between different files. Still, it was a mistake, and also caused part of #44176 to not work in certain edge cases (see the test added to completionsIncomplete.ts).

This PR also renames and relocates a lot of things to make more sense. I’ll point out where the significant changes are in review comments.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Jun 29, 2021
@@ -0,0 +1,95 @@
/*@internal*/
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved verbatim from utilities.ts

Comment on lines -319 to -347
const result: MultiMap<string, SymbolExportInfo> = createMultiMap();
const compilerOptions = program.getCompilerOptions();
const target = getEmitScriptTarget(compilerOptions);
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, _moduleFile, program, isFromPackageJson) => {
const checker = program.getTypeChecker();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && !checker.isUndefinedSymbol(defaultInfo.symbol)) {
const name = getNameForExportedSymbol(getLocalSymbolForExportDefault(defaultInfo.symbol) || defaultInfo.symbol, target);
result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), {
symbol: defaultInfo.symbol,
moduleSymbol,
exportKind: defaultInfo.exportKind,
exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker),
isFromPackageJson,
});
}
const seenExports = new Map<Symbol, true>();
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
if (exported !== defaultInfo?.symbol && addToSeen(seenExports, exported)) {
result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), {
symbol: exported,
moduleSymbol,
exportKind: ExportKind.Named,
exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker),
isFromPackageJson,
});
}
}
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This core part of getSymbolToExportInfoMap moved to exportInfoMap.ts mostly unchanged.

Comment on lines -370 to -399
function wrapMultiMap(map: MultiMap<string, SymbolExportInfo>, isFromPreviousProjectVersion: boolean): SymbolToExportInfoMap {
const wrapped: SymbolToExportInfoMap = {
get: (importedName, symbol, moduleSymbol, checker) => {
const info = map.get(key(importedName, symbol, moduleSymbol, checker));
return isFromPreviousProjectVersion ? info?.map(info => replaceTransientSymbols(info, checker)) : info;
},
forEach: (getChecker, action) => {
map.forEach((info, key) => {
const { symbolName, ambientModuleName } = parseKey(key);
action(
isFromPreviousProjectVersion ? info.map(i => replaceTransientSymbols(i, getChecker(i.isFromPackageJson))) : info,
symbolName,
!!ambientModuleName);
});
},
};
if (Debug.isDebugging) {
Object.defineProperty(wrapped, "__cache", { get: () => map });
}
return wrapped;

function replaceTransientSymbols(info: SymbolExportInfo, checker: TypeChecker) {
if (info.symbol.flags & SymbolFlags.Transient) {
info.symbol = checker.getMergedSymbol(info.symbol.declarations?.[0]?.symbol || info.symbol);
}
if (info.moduleSymbol.flags & SymbolFlags.Transient) {
info.moduleSymbol = checker.getMergedSymbol(info.moduleSymbol.declarations?.[0]?.symbol || info.moduleSymbol);
}
return info;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main part of the code that was rewritten, but there are conceptual analogs present in exportInfoMap.ts.

@@ -773,21 +667,6 @@ namespace ts.codefix {
return originalSymbolToExportInfos;
}

function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of the removals in this file just moved to exportInfoMap.ts unchanged.

src/services/exportInfoMap.ts Show resolved Hide resolved
}
return cache;

function rehydrateCachedInfo(info: CachedSymbolExportInfo): SymbolExportInfo {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function, wrapped by the get and forEach methods of the returned cache, turn CachedSymbolExportInfo, which might be missing symbol or moduleSymbol if they were transient, by pulling those from the short-term symbols cache, or falling back to fetching fresh versions of them from the current program.

Comment on lines +84 to +99
const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol;
const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol;
if (!storedSymbol || !storedModuleSymbol) symbols.set(id, [symbol, moduleSymbol]);

exportInfo.add(key(importedName, symbol, moduleName, checker), {
id,
symbolName: importedName,
moduleName,
moduleFile,
moduleFileName: moduleFile?.fileName,
exportKind,
isTypeOnly: !(skipAlias(symbol, checker).flags & SymbolFlags.Value),
isFromPackageJson,
symbol: storedSymbol,
moduleSymbol: storedModuleSymbol,
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symbols stored in symbols if transient, exportInfo if not. I tried never storing any symbols, but the performance hit was too great.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable.

src/server/project.ts Outdated Show resolved Hide resolved
src/services/exportInfoMap.ts Outdated Show resolved Hide resolved
src/services/completions.ts Outdated Show resolved Hide resolved
src/services/completions.ts Outdated Show resolved Hide resolved
src/services/exportInfoMap.ts Show resolved Hide resolved
const { exportMapCache, checker } = setup();
assert.ok(exportMapCache.get(bTs.path as Path, checker));
const { exportMapCache } = setup();
assert.ok(exportMapCache.isUsableByFile(bTs.path as Path));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably out of scope, but I would find isTrue and isFalse more readable.

src/testRunner/unittests/tsserver/exportMapCache.ts Outdated Show resolved Hide resolved
src/testRunner/unittests/tsserver/exportMapCache.ts Outdated Show resolved Hide resolved
@andrewbranch andrewbranch merged commit 1da18c6 into microsoft:main Jul 6, 2021
@andrewbranch andrewbranch deleted the fix-cache branch July 6, 2021 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants