Use cache for module resolution even in watch mode

This commit is contained in:
Sheetal Nandi 2017-08-04 01:14:54 -07:00
parent 031a63762f
commit 0d5e6c9de5
5 changed files with 244 additions and 177 deletions

View file

@ -0,0 +1,198 @@
/// <reference path="types.ts"/>
/// <reference path="core.ts"/>
namespace ts {
export interface ResolutionCache {
setModuleResolutionHost(host: ModuleResolutionHost): void;
startRecordingFilesWithChangedResolutions(): void;
finishRecordingFilesWithChangedResolutions(): Path[];
resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[];
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
invalidateResolutionOfDeletedFile(filePath: Path): void;
clear(): void;
}
type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean };
type ResolverWithGlobalCache = (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost) => ResolvedModuleWithFailedLookupLocations | undefined;
/*@internal*/
export function resolveWithGlobalCache(primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string | undefined, projectName: string): ResolvedModuleWithFailedLookupLocations | undefined {
if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) {
// otherwise try to load typings from @types
// create different collection of failed lookup locations for second pass
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache);
if (resolvedModule) {
return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) };
}
}
}
/*@internal*/
export function createResolutionCache(
toPath: (fileName: string) => Path,
getCompilerOptions: () => CompilerOptions,
resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache {
let host: ModuleResolutionHost;
let filesWithChangedSetOfUnresolvedImports: Path[];
const resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
const resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
return {
setModuleResolutionHost,
startRecordingFilesWithChangedResolutions,
finishRecordingFilesWithChangedResolutions,
resolveModuleNames,
resolveTypeReferenceDirectives,
invalidateResolutionOfDeletedFile,
clear
};
function setModuleResolutionHost(updatedHost: ModuleResolutionHost) {
host = updatedHost;
}
function clear() {
resolvedModuleNames.clear();
resolvedTypeReferenceDirectives.clear();
}
function startRecordingFilesWithChangedResolutions() {
filesWithChangedSetOfUnresolvedImports = [];
}
function finishRecordingFilesWithChangedResolutions() {
const collected = filesWithChangedSetOfUnresolvedImports;
filesWithChangedSetOfUnresolvedImports = undefined;
return collected;
}
function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host);
// return result immediately only if it is .ts, .tsx or .d.ts
// otherwise try to load typings from @types
return (resolveWithGlobalCache && resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host)) || primaryResult;
}
function resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R>(
names: string[],
containingFile: string,
cache: Map<Map<T>>,
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined,
logChanges: boolean): R[] {
const path = toPath(containingFile);
const currentResolutionsInFile = cache.get(path);
const newResolutions: Map<T> = createMap<T>();
const resolvedModules: R[] = [];
const compilerOptions = getCompilerOptions();
for (const name of names) {
// check if this is a duplicate entry in the list
let resolution = newResolutions.get(name);
if (!resolution) {
const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name);
if (moduleResolutionIsValid(existingResolution)) {
// ok, it is safe to use existing name resolution results
resolution = existingResolution;
}
else {
resolution = loader(name, containingFile, compilerOptions, host);
newResolutions.set(name, resolution);
}
if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
filesWithChangedSetOfUnresolvedImports.push(path);
// reset log changes to avoid recording the same file multiple times
logChanges = false;
}
}
Debug.assert(resolution !== undefined);
resolvedModules.push(getResult(resolution));
}
// replace old results with a new one
cache.set(path, newResolutions);
return resolvedModules;
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
if (oldResolution === newResolution) {
return true;
}
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
return false;
}
const oldResult = getResult(oldResolution);
const newResult = getResult(newResolution);
if (oldResult === newResult) {
return true;
}
if (!oldResult || !newResult) {
return false;
}
return getResultFileName(oldResult) === getResultFileName(newResult);
}
function moduleResolutionIsValid(resolution: T): boolean {
if (!resolution || resolution.isInvalidated) {
return false;
}
const result = getResult(resolution);
if (result) {
return true;
}
// consider situation if we have no candidate locations as valid resolution.
// after all there is no point to invalidate it if we have no idea where to look for the module.
return resolution.failedLookupLocations.length === 0;
}
}
function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
}
function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] {
return resolveNamesWithLocalCache(moduleNames, containingFile, resolvedModuleNames, resolveModuleName,
m => m.resolvedModule, r => r.resolvedFileName, logChanges);
}
function invalidateResolutionCacheOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R>(
deletedFilePath: Path,
cache: Map<Map<T>>,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined) {
cache.forEach((value, path) => {
if (path === deletedFilePath) {
cache.delete(path);
}
else if (value) {
value.forEach((resolution) => {
if (resolution && !resolution.isInvalidated) {
const result = getResult(resolution);
if (result) {
if (getResultFileName(result) === deletedFilePath) {
resolution.isInvalidated = true;
}
}
}
});
}
});
}
function invalidateResolutionOfDeletedFile(filePath: Path) {
invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName);
invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
}
}
}

View file

@ -37,6 +37,7 @@
"emitter.ts",
"program.ts",
"builder.ts",
"resolutionCache.ts",
"watchedProgram.ts",
"commandLineParser.ts",
"tsc.ts",

View file

@ -1,5 +1,6 @@
/// <reference path="program.ts" />
/// <reference path="builder.ts" />
/// <reference path="resolutionCache.ts"/>
namespace ts {
export type DiagnosticReporter = (diagnostic: Diagnostic) => void;
@ -254,6 +255,7 @@ namespace ts {
let timerToUpdateProgram: any; // timer callback to recompile the program
const sourceFilesCache = createMap<HostFileInfo | string>(); // Cache that stores the source file and version info
watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty);
const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost;
@ -268,6 +270,9 @@ namespace ts {
const currentDirectory = host.getCurrentDirectory();
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
// Cache for the module resolution
const resolutionCache = createResolutionCache(fileName => toPath(fileName), () => compilerOptions);
// There is no extra check needed since we can just rely on the program to decide emit
const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true);
@ -287,6 +292,10 @@ namespace ts {
// Create the compiler host
const compilerHost = createWatchedCompilerHost(compilerOptions);
resolutionCache.setModuleResolutionHost(compilerHost);
if (changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) {
resolutionCache.clear();
}
beforeCompile(compilerOptions);
// Compile the program
@ -321,22 +330,18 @@ namespace ts {
getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "",
getDirectories: (path: string) => host.getDirectories(path),
realpath,
onReleaseOldSourceFile,
resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile),
resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false),
onReleaseOldSourceFile
};
}
// TODO: cache module resolution
// if (host.resolveModuleNames) {
// compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile);
// }
// if (host.resolveTypeReferenceDirectives) {
// compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => {
// return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile);
// };
// }
function toPath(fileName: string) {
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
}
function fileExists(fileName: string) {
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const path = toPath(fileName);
const hostSourceFileInfo = sourceFilesCache.get(path);
if (hostSourceFileInfo !== undefined) {
return !isString(hostSourceFileInfo);
@ -350,7 +355,7 @@ namespace ts {
}
function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile);
return getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile);
}
function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
@ -418,6 +423,7 @@ namespace ts {
if (hostSourceFile !== undefined) {
if (!isString(hostSourceFile)) {
hostSourceFile.fileWatcher.close();
resolutionCache.invalidateResolutionOfDeletedFile(path);
}
sourceFilesCache.delete(path);
}
@ -501,6 +507,7 @@ namespace ts {
if (hostSourceFile) {
// Update the cache
if (eventKind === FileWatcherEventKind.Deleted) {
resolutionCache.invalidateResolutionOfDeletedFile(path);
if (!isString(hostSourceFile)) {
hostSourceFile.fileWatcher.close();
sourceFilesCache.set(path, (hostSourceFile.version++).toString());
@ -574,7 +581,7 @@ namespace ts {
function onFileAddOrRemoveInWatchedDirectory(fileName: string) {
Debug.assert(!!configFileName);
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const path = toPath(fileName);
// Since the file existance changed, update the sourceFiles cache
updateCachedSystem(fileName, path);

View file

@ -1,6 +1,7 @@
/// <reference path="..\services\services.ts" />
/// <reference path="utilities.ts" />
/// <reference path="scriptInfo.ts" />
/// <reference path="..\compiler\resolutionCache.ts" />
namespace ts.server {
export class CachedServerHost implements ServerHost {
@ -103,15 +104,10 @@ namespace ts.server {
}
type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean };
export class LSHost implements LanguageServiceHost, ModuleResolutionHost {
private compilationSettings: CompilerOptions;
private readonly resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
private readonly resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
/*@internal*/
compilationSettings: CompilerOptions;
private filesWithChangedSetOfUnresolvedImports: Path[];
private resolveModuleName: typeof resolveModuleName;
readonly trace: (s: string) => void;
readonly realpath?: (path: string) => string;
/**
@ -130,25 +126,6 @@ namespace ts.server {
this.trace = s => host.trace(s);
}
this.resolveModuleName = (moduleName, containingFile, compilerOptions, host) => {
const globalCache = this.project.getTypeAcquisition().enable
? this.project.projectService.typingsInstaller.globalTypingsCacheLocation
: undefined;
const primaryResult = resolveModuleName(moduleName, containingFile, compilerOptions, host);
// return result immediately only if it is .ts, .tsx or .d.ts
if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) {
// otherwise try to load typings from @types
// create different collection of failed lookup locations for second pass
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, this.project.getProjectName(), compilerOptions, host, globalCache);
if (resolvedModule) {
return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) };
}
}
return primaryResult;
};
if (this.host.realpath) {
this.realpath = path => this.host.realpath(path);
}
@ -156,99 +133,9 @@ namespace ts.server {
dispose() {
this.project = undefined;
this.resolveModuleName = undefined;
this.host = undefined;
}
public startRecordingFilesWithChangedResolutions() {
this.filesWithChangedSetOfUnresolvedImports = [];
}
public finishRecordingFilesWithChangedResolutions() {
const collected = this.filesWithChangedSetOfUnresolvedImports;
this.filesWithChangedSetOfUnresolvedImports = undefined;
return collected;
}
private resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R>(
names: string[],
containingFile: string,
cache: Map<Map<T>>,
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined,
logChanges: boolean): R[] {
const path = this.project.projectService.toPath(containingFile);
const currentResolutionsInFile = cache.get(path);
const newResolutions: Map<T> = createMap<T>();
const resolvedModules: R[] = [];
const compilerOptions = this.getCompilationSettings();
for (const name of names) {
// check if this is a duplicate entry in the list
let resolution = newResolutions.get(name);
if (!resolution) {
const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name);
if (moduleResolutionIsValid(existingResolution)) {
// ok, it is safe to use existing name resolution results
resolution = existingResolution;
}
else {
resolution = loader(name, containingFile, compilerOptions, this);
newResolutions.set(name, resolution);
}
if (logChanges && this.filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
this.filesWithChangedSetOfUnresolvedImports.push(path);
// reset log changes to avoid recording the same file multiple times
logChanges = false;
}
}
Debug.assert(resolution !== undefined);
resolvedModules.push(getResult(resolution));
}
// replace old results with a new one
cache.set(path, newResolutions);
return resolvedModules;
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
if (oldResolution === newResolution) {
return true;
}
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
return false;
}
const oldResult = getResult(oldResolution);
const newResult = getResult(newResolution);
if (oldResult === newResult) {
return true;
}
if (!oldResult || !newResult) {
return false;
}
return getResultFileName(oldResult) === getResultFileName(newResult);
}
function moduleResolutionIsValid(resolution: T): boolean {
if (!resolution || resolution.isInvalidated) {
return false;
}
const result = getResult(resolution);
if (result) {
return true;
}
// consider situation if we have no candidate locations as valid resolution.
// after all there is no point to invalidate it if we have no idea where to look for the module.
return resolution.failedLookupLocations.length === 0;
}
}
getNewLine() {
return this.host.newLine;
}
@ -270,13 +157,11 @@ namespace ts.server {
}
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
return this.project.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile);
}
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] {
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName,
m => m.resolvedModule, r => r.resolvedFileName, /*logChanges*/ true);
return this.project.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true);
}
getDefaultLibFileName() {
@ -339,44 +224,5 @@ namespace ts.server {
getDirectories(path: string): string[] {
return this.host.getDirectories(path);
}
notifyFileRemoved(info: ScriptInfo) {
this.invalidateResolutionOfDeletedFile(info, this.resolvedModuleNames,
m => m.resolvedModule, r => r.resolvedFileName);
this.invalidateResolutionOfDeletedFile(info, this.resolvedTypeReferenceDirectives,
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
}
private invalidateResolutionOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R>(
deletedInfo: ScriptInfo,
cache: Map<Map<T>>,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined) {
cache.forEach((value, path) => {
if (path === deletedInfo.path) {
cache.delete(path);
}
else if (value) {
value.forEach((resolution) => {
if (resolution && !resolution.isInvalidated) {
const result = getResult(resolution);
if (result) {
if (getResultFileName(result) === deletedInfo.path) {
resolution.isInvalidated = true;
}
}
}
});
}
});
}
setCompilationSettings(opt: CompilerOptions) {
if (changesAffectModuleResolution(this.compilationSettings, opt)) {
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
this.compilationSettings = opt;
}
}
}

View file

@ -127,6 +127,9 @@ namespace ts.server {
public languageServiceEnabled = true;
/*@internal*/
resolutionCache: ResolutionCache;
/*@internal*/
lsHost: LSHost;
@ -211,7 +214,14 @@ namespace ts.server {
this.setInternalCompilerOptionsForEmittingJsFiles();
this.lsHost = new LSHost(host, this, this.projectService.cancellationToken);
this.lsHost.setCompilationSettings(this.compilerOptions);
this.resolutionCache = createResolutionCache(
fileName => this.projectService.toPath(fileName),
() => this.compilerOptions,
(primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host,
this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName())
);
this.lsHost.compilationSettings = this.compilerOptions;
this.resolutionCache.setModuleResolutionHost(this.lsHost);
this.languageService = createLanguageService(this.lsHost, this.documentRegistry);
@ -349,6 +359,7 @@ namespace ts.server {
this.rootFilesMap = undefined;
this.program = undefined;
this.builder = undefined;
this.resolutionCache = undefined;
this.cachedUnresolvedImportsPerFile = undefined;
this.projectErrors = undefined;
this.lsHost.dispose();
@ -518,7 +529,7 @@ namespace ts.server {
if (this.isRoot(info)) {
this.removeRoot(info);
}
this.lsHost.notifyFileRemoved(info);
this.resolutionCache.invalidateResolutionOfDeletedFile(info.path);
this.cachedUnresolvedImportsPerFile.remove(info.path);
if (detachFromProject) {
@ -573,11 +584,11 @@ namespace ts.server {
* @returns: true if set of files in the project stays the same and false - otherwise.
*/
updateGraph(): boolean {
this.lsHost.startRecordingFilesWithChangedResolutions();
this.resolutionCache.startRecordingFilesWithChangedResolutions();
let hasChanges = this.updateGraphWorker();
const changedFiles: ReadonlyArray<Path> = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray;
const changedFiles: ReadonlyArray<Path> = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray;
for (const file of changedFiles) {
// delete cached information for changed files
@ -759,9 +770,13 @@ namespace ts.server {
this.cachedUnresolvedImportsPerFile.clear();
this.lastCachedUnresolvedImportsList = undefined;
}
const oldOptions = this.compilerOptions;
this.compilerOptions = compilerOptions;
this.setInternalCompilerOptionsForEmittingJsFiles();
this.lsHost.setCompilationSettings(compilerOptions);
if (changesAffectModuleResolution(oldOptions, compilerOptions)) {
this.resolutionCache.clear();
}
this.lsHost.compilationSettings = this.compilerOptions;
this.markAsDirty();
}