Use changeCompilerHostLikeToUseCache in synchronizeHostData (#48980)

* Remove unnecessary members of HostCache

* Standardize on calling compiler host members in preparation for adding caching at that layer

* Call changeCompilerHostLikeToUseCache to cache existence checks

* Drop now-redundant HostCache

* Don't make directoryExists caching contingent on createDirectory

* Clear compilerHost rather than tracking state
This commit is contained in:
Andrew Casey 2022-05-18 17:26:55 -07:00 committed by GitHub
parent 12ed01203c
commit 5aa0053c74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 62 additions and 149 deletions

View File

@ -282,7 +282,7 @@ namespace ts {
}
// directoryExists
if (originalDirectoryExists && originalCreateDirectory) {
if (originalDirectoryExists) {
host.directoryExists = directory => {
const key = toPath(directory);
const value = directoryExistsCache.get(key);
@ -291,11 +291,14 @@ namespace ts {
directoryExistsCache.set(key, !!newValue);
return newValue;
};
host.createDirectory = directory => {
const key = toPath(directory);
directoryExistsCache.delete(key);
originalCreateDirectory.call(host, directory);
};
if (originalCreateDirectory) {
host.createDirectory = directory => {
const key = toPath(directory);
directoryExistsCache.delete(key);
originalCreateDirectory.call(host, directory);
};
}
}
return {

View File

@ -927,14 +927,6 @@ namespace ts {
/// Language Service
// Information about a specific host file.
interface HostFileInformation {
hostFileName: string;
version: string;
scriptSnapshot: IScriptSnapshot;
scriptKind: ScriptKind;
}
/* @internal */
export interface DisplayPartsSymbolWriter extends EmitTextWriter {
displayParts(): SymbolDisplayPart[];
@ -988,82 +980,6 @@ namespace ts {
return codefix.getSupportedErrorCodes();
}
// Either it will be file name if host doesnt have file or it will be the host's file information
type CachedHostFileInformation = HostFileInformation | string;
// Cache host information about script Should be refreshed
// at each language service public entry point, since we don't know when
// the set of scripts handled by the host changes.
class HostCache {
private fileNameToEntry: ESMap<Path, CachedHostFileInformation>;
private currentDirectory: string;
constructor(private host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName) {
// script id => script index
this.currentDirectory = host.getCurrentDirectory();
this.fileNameToEntry = new Map();
// Initialize the list with the root file names
const rootFileNames = host.getScriptFileNames();
tracing?.push(tracing.Phase.Session, "initializeHostCache", { count: rootFileNames.length });
for (const fileName of rootFileNames) {
this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName));
}
tracing?.pop();
}
private createEntry(fileName: string, path: Path) {
let entry: CachedHostFileInformation;
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
if (scriptSnapshot) {
entry = {
hostFileName: fileName,
version: this.host.getScriptVersion(fileName),
scriptSnapshot,
scriptKind: getScriptKind(fileName, this.host)
};
}
else {
entry = fileName;
}
this.fileNameToEntry.set(path, entry);
return entry;
}
public getEntryByPath(path: Path): CachedHostFileInformation | undefined {
return this.fileNameToEntry.get(path);
}
public getHostFileInformation(path: Path): HostFileInformation | undefined {
const entry = this.fileNameToEntry.get(path);
return !isString(entry) ? entry : undefined;
}
public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation {
const info = this.getEntryByPath(path) || this.createEntry(fileName, path);
return isString(info) ? undefined! : info; // TODO: GH#18217
}
public getRootFileNames(): string[] {
const names: string[] = [];
this.fileNameToEntry.forEach(entry => {
if (isString(entry)) {
names.push(entry);
}
else {
names.push(entry.hostFileName);
}
});
return names;
}
public getScriptSnapshot(path: Path): IScriptSnapshot {
const file = this.getHostFileInformation(path);
return (file && file.scriptSnapshot)!; // TODO: GH#18217
}
}
class SyntaxTreeCache {
// For our syntactic only features, we also keep a cache of the syntax tree for the
// currently edited file.
@ -1367,37 +1283,17 @@ namespace ts {
lastTypesRootVersion = typeRootsVersion;
}
const rootFileNames = host.getScriptFileNames();
// Get a fresh cache of the host information
let hostCache: HostCache | undefined = new HostCache(host, getCanonicalFileName);
const rootFileNames = hostCache.getRootFileNames();
const newSettings = host.getCompilationSettings() || getDefaultCompilerOptions();
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
const hasChangedAutomaticTypeDirectiveNames = maybeBind(host, host.hasChangedAutomaticTypeDirectiveNames);
const projectReferences = host.getProjectReferences?.();
let parsedCommandLines: ESMap<Path, ParsedCommandLine | false> | undefined;
const parseConfigHost: ParseConfigFileHost = {
useCaseSensitiveFileNames,
fileExists,
readFile,
readDirectory,
trace: maybeBind(host, host.trace),
getCurrentDirectory: () => currentDirectory,
onUnRecoverableConfigFileDiagnostic: noop,
};
// If the program is already up-to-date, we can reuse it
if (isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
return;
}
// IMPORTANT - It is critical from this moment onward that we do not check
// cancellation tokens. We are about to mutate source files from a previous program
// instance. If we cancel midway through, we may end up in an inconsistent state where
// the program points to old source files that have been invalidated because of
// incremental parsing.
// Now create a new compiler
const compilerHost: CompilerHost = {
let compilerHost: CompilerHost | undefined = {
getSourceFile: getOrCreateSourceFile,
getSourceFileByPath: getOrCreateSourceFileByPath,
getCancellationToken: () => cancellationToken,
@ -1407,8 +1303,8 @@ namespace ts {
getDefaultLibFileName: options => host.getDefaultLibFileName(options),
writeFile: noop,
getCurrentDirectory: () => currentDirectory,
fileExists,
readFile,
fileExists: fileName => host.fileExists(fileName),
readFile: fileName => host.readFile && host.readFile(fileName),
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
realpath: maybeBind(host, host.realpath),
directoryExists: directoryName => {
@ -1417,20 +1313,54 @@ namespace ts {
getDirectories: path => {
return host.getDirectories ? host.getDirectories(path) : [];
},
readDirectory,
readDirectory: (path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) => {
Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'");
return host.readDirectory!(path, extensions, exclude, include, depth);
},
onReleaseOldSourceFile,
onReleaseParsedCommandLine,
hasInvalidatedResolution,
hasChangedAutomaticTypeDirectiveNames,
trace: parseConfigHost.trace,
trace: maybeBind(host, host.trace),
resolveModuleNames: maybeBind(host, host.resolveModuleNames),
getModuleResolutionCache: maybeBind(host, host.getModuleResolutionCache),
resolveTypeReferenceDirectives: maybeBind(host, host.resolveTypeReferenceDirectives),
useSourceOfProjectReferenceRedirect: maybeBind(host, host.useSourceOfProjectReferenceRedirect),
getParsedCommandLine,
};
const originalGetSourceFile = compilerHost.getSourceFile;
const { getSourceFileWithCache } = changeCompilerHostLikeToUseCache(
compilerHost,
fileName => toPath(fileName, currentDirectory, getCanonicalFileName),
(...args) => originalGetSourceFile.call(compilerHost, ...args)
);
compilerHost.getSourceFile = getSourceFileWithCache!;
host.setCompilerHost?.(compilerHost);
const parseConfigHost: ParseConfigFileHost = {
useCaseSensitiveFileNames,
fileExists: fileName => compilerHost!.fileExists(fileName),
readFile: fileName => compilerHost!.readFile(fileName),
readDirectory: (...args) => compilerHost!.readDirectory!(...args),
trace: compilerHost.trace,
getCurrentDirectory: compilerHost.getCurrentDirectory,
onUnRecoverableConfigFileDiagnostic: noop,
};
// If the program is already up-to-date, we can reuse it
if (isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileName => compilerHost!.fileExists(fileName), hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
return;
}
// IMPORTANT - It is critical from this moment onward that we do not check
// cancellation tokens. We are about to mutate source files from a previous program
// instance. If we cancel midway through, we may end up in an inconsistent state where
// the program points to old source files that have been invalidated because of
// incremental parsing.
const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
const options: CreateProgramOptions = {
rootNames: rootFileNames,
@ -1441,9 +1371,9 @@ namespace ts {
};
program = createProgram(options);
// hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point.
// It needs to be cleared to allow all collected snapshots to be released
hostCache = undefined;
// 'getOrCreateSourceFile' depends on caching but should be used past this point.
// After this point, the cache needs to be cleared to allow all collected snapshots to be released
compilerHost = undefined;
parsedCommandLines = undefined;
// We reset this cache on structure invalidation so we don't hold on to outdated files for long; however we can't use the `compilerHost` above,
@ -1492,29 +1422,6 @@ namespace ts {
}
}
function fileExists(fileName: string): boolean {
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const entry = hostCache && hostCache.getEntryByPath(path);
return entry ?
!isString(entry) :
(!!host.fileExists && host.fileExists(fileName));
}
function readFile(fileName: string) {
// stub missing host functionality
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const entry = hostCache && hostCache.getEntryByPath(path);
if (entry) {
return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot);
}
return host.readFile && host.readFile(fileName);
}
function readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) {
Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'");
return host.readDirectory!(path, extensions, exclude, include, depth);
}
// Release any files we have acquired in the old program but are
// not part of the new program.
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {
@ -1527,15 +1434,18 @@ namespace ts {
}
function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined {
Debug.assert(hostCache !== undefined, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host.");
Debug.assert(compilerHost, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host.");
// The program is asking for this file, check first if the host can locate it.
// If the host can not locate the file, then it does not exist. return undefined
// to the program to allow reporting of errors for missing files.
const hostFileInformation = hostCache && hostCache.getOrCreateEntryByPath(fileName, path);
if (!hostFileInformation) {
const scriptSnapshot = host.getScriptSnapshot(fileName);
if (!scriptSnapshot) {
return undefined;
}
const scriptKind = getScriptKind(fileName, host);
const scriptVersion = host.getScriptVersion(fileName);
// Check if the language version has changed since we last created a program; if they are the same,
// it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile
// can not be reused. we have to dump all syntax trees and create new ones.
@ -1568,8 +1478,8 @@ namespace ts {
// We do not support the scenario where a host can modify a registered
// file's script kind, i.e. in one project some file is treated as ".ts"
// and in another as ".js"
if (hostFileInformation.scriptKind === oldSourceFile.scriptKind) {
return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
if (scriptKind === oldSourceFile.scriptKind) {
return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, scriptSnapshot, scriptVersion, scriptKind);
}
else {
// Release old source file and fall through to aquire new file with new script kind
@ -1581,7 +1491,7 @@ namespace ts {
}
// Could not find this file in the old program, create a new SourceFile for it.
return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, scriptSnapshot, scriptVersion, scriptKind);
}
}