-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Conversation
@@ -0,0 +1,95 @@ | |||
/*@internal*/ |
There was a problem hiding this comment.
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
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, | ||
}); | ||
} | ||
} | ||
}); |
There was a problem hiding this comment.
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.
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; | ||
} |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
} | ||
return cache; | ||
|
||
function rehydrateCachedInfo(info: CachedSymbolExportInfo): SymbolExportInfo { |
There was a problem hiding this comment.
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.
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, | ||
}); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems reasonable.
const { exportMapCache, checker } = setup(); | ||
assert.ok(exportMapCache.get(bTs.path as Path, checker)); | ||
const { exportMapCache } = setup(); | ||
assert.ok(exportMapCache.isUsableByFile(bTs.path as Path)); |
There was a problem hiding this comment.
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.
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.