Merge pull request #24206 from Microsoft/documentRegistery
Cache the latest source file from document registery in script info so that we do not have to reparse orphan script info
This commit is contained in:
commit
f44dd6f53b
|
@ -407,6 +407,12 @@ namespace ts.projectSystem {
|
|||
checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path).fileName), expectedFiles.map(file => file.path));
|
||||
}
|
||||
|
||||
function textSpanFromSubstring(str: string, substring: string): TextSpan {
|
||||
const start = str.indexOf(substring);
|
||||
Debug.assert(start !== -1);
|
||||
return createTextSpan(start, substring.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test server cancellation token used to mock host token cancellation requests.
|
||||
* The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls
|
||||
|
@ -8420,9 +8426,96 @@ new C();`
|
|||
});
|
||||
});
|
||||
|
||||
function textSpanFromSubstring(str: string, substring: string): TextSpan {
|
||||
const start = str.indexOf(substring);
|
||||
Debug.assert(start !== -1);
|
||||
return createTextSpan(start, substring.length);
|
||||
}
|
||||
describe("document registry in project service", () => {
|
||||
const projectRootPath = "/user/username/projects/project";
|
||||
const importModuleContent = `import {a} from "./module1"`;
|
||||
const file: File = {
|
||||
path: `${projectRootPath}/index.ts`,
|
||||
content: importModuleContent
|
||||
};
|
||||
const moduleFile: File = {
|
||||
path: `${projectRootPath}/module1.d.ts`,
|
||||
content: "export const a: number;"
|
||||
};
|
||||
const configFile: File = {
|
||||
path: `${projectRootPath}/tsconfig.json`,
|
||||
content: JSON.stringify({ files: ["index.ts"] })
|
||||
};
|
||||
|
||||
function getProject(service: TestProjectService) {
|
||||
return service.configuredProjects.get(configFile.path);
|
||||
}
|
||||
|
||||
function checkProject(service: TestProjectService, moduleIsOrphan: boolean) {
|
||||
// Update the project
|
||||
const project = getProject(service);
|
||||
project.getLanguageService();
|
||||
checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]);
|
||||
const moduleInfo = service.getScriptInfo(moduleFile.path);
|
||||
assert.isDefined(moduleInfo);
|
||||
assert.equal(moduleInfo.isOrphan(), moduleIsOrphan);
|
||||
const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings());
|
||||
assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]);
|
||||
}
|
||||
|
||||
function createServiceAndHost() {
|
||||
const host = createServerHost([file, moduleFile, libFile, configFile]);
|
||||
const service = createProjectService(host);
|
||||
service.openClientFile(file.path);
|
||||
checkProject(service, /*moduleIsOrphan*/ false);
|
||||
return { host, service };
|
||||
}
|
||||
|
||||
function changeFileToNotImportModule(service: TestProjectService) {
|
||||
const info = service.getScriptInfo(file.path);
|
||||
service.applyChangesToFile(info, [{ span: { start: 0, length: importModuleContent.length }, newText: "" }]);
|
||||
checkProject(service, /*moduleIsOrphan*/ true);
|
||||
}
|
||||
|
||||
function changeFileToImportModule(service: TestProjectService) {
|
||||
const info = service.getScriptInfo(file.path);
|
||||
service.applyChangesToFile(info, [{ span: { start: 0, length: 0 }, newText: importModuleContent }]);
|
||||
checkProject(service, /*moduleIsOrphan*/ false);
|
||||
}
|
||||
|
||||
it("Caches the source file if script info is orphan", () => {
|
||||
const { service } = createServiceAndHost();
|
||||
const project = getProject(service);
|
||||
|
||||
const moduleInfo = service.getScriptInfo(moduleFile.path);
|
||||
const sourceFile = moduleInfo.cacheSourceFile.sourceFile;
|
||||
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
|
||||
|
||||
// edit file
|
||||
changeFileToNotImportModule(service);
|
||||
assert.equal(moduleInfo.cacheSourceFile.sourceFile, sourceFile);
|
||||
|
||||
// write content back
|
||||
changeFileToImportModule(service);
|
||||
assert.equal(moduleInfo.cacheSourceFile.sourceFile, sourceFile);
|
||||
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
|
||||
});
|
||||
|
||||
it("Caches the source file if script info is orphan, and orphan script info changes", () => {
|
||||
const { host, service } = createServiceAndHost();
|
||||
const project = getProject(service);
|
||||
|
||||
const moduleInfo = service.getScriptInfo(moduleFile.path);
|
||||
const sourceFile = moduleInfo.cacheSourceFile.sourceFile;
|
||||
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
|
||||
|
||||
// edit file
|
||||
changeFileToNotImportModule(service);
|
||||
assert.equal(moduleInfo.cacheSourceFile.sourceFile, sourceFile);
|
||||
|
||||
const updatedModuleContent = moduleFile.content + "\nexport const b: number;";
|
||||
host.writeFile(moduleFile.path, updatedModuleContent);
|
||||
|
||||
// write content back
|
||||
changeFileToImportModule(service);
|
||||
assert.notEqual(moduleInfo.cacheSourceFile.sourceFile, sourceFile);
|
||||
assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile.sourceFile);
|
||||
assert.equal(moduleInfo.cacheSourceFile.sourceFile.text, updatedModuleContent);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -339,7 +339,8 @@ namespace ts.server {
|
|||
/*@internal*/
|
||||
readonly typingsCache: TypingsCache;
|
||||
|
||||
private readonly documentRegistry: DocumentRegistry;
|
||||
/*@internal*/
|
||||
readonly documentRegistry: DocumentRegistry;
|
||||
|
||||
/**
|
||||
* Container of all known scripts
|
||||
|
@ -474,7 +475,7 @@ namespace ts.server {
|
|||
extraFileExtensions: []
|
||||
};
|
||||
|
||||
this.documentRegistry = createDocumentRegistry(this.host.useCaseSensitiveFileNames, this.currentDirectory);
|
||||
this.documentRegistry = createDocumentRegistryInternal(this.host.useCaseSensitiveFileNames, this.currentDirectory, this);
|
||||
const watchLogLevel = this.logger.hasLevel(LogLevel.verbose) ? WatchLogLevel.Verbose :
|
||||
this.logger.loggingEnabled() ? WatchLogLevel.TriggerOnly : WatchLogLevel.None;
|
||||
const log: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => this.logger.info(s)) : noop;
|
||||
|
@ -495,6 +496,19 @@ namespace ts.server {
|
|||
return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory());
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile) {
|
||||
const info = this.getScriptInfoForPath(path);
|
||||
Debug.assert(!!info);
|
||||
info.cacheSourceFile = { key, sourceFile };
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
getDocument(key: DocumentRegistryBucketKey, path: Path) {
|
||||
const info = this.getScriptInfoForPath(path);
|
||||
return info && info.cacheSourceFile && info.cacheSourceFile.key === key && info.cacheSourceFile.sourceFile;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
ensureInferredProjectsUpToDate_TestOnly() {
|
||||
this.ensureProjectStructuresUptoDate();
|
||||
|
|
|
@ -202,6 +202,12 @@ namespace ts.server {
|
|||
return fileName[0] === "^" || getBaseFileName(fileName)[0] === "^";
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
export interface DocumentRegistrySourceFileCache {
|
||||
key: DocumentRegistryBucketKey;
|
||||
sourceFile: SourceFile;
|
||||
}
|
||||
|
||||
export class ScriptInfo {
|
||||
/**
|
||||
* All projects that include this file
|
||||
|
@ -221,6 +227,9 @@ namespace ts.server {
|
|||
/** Set to real path if path is different from info.path */
|
||||
private realpath: Path | undefined;
|
||||
|
||||
/*@internal*/
|
||||
cacheSourceFile: DocumentRegistrySourceFileCache;
|
||||
|
||||
constructor(
|
||||
private readonly host: ServerHost,
|
||||
readonly fileName: NormalizedPath,
|
||||
|
|
|
@ -87,9 +87,18 @@ namespace ts {
|
|||
|
||||
releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void;
|
||||
|
||||
/*@internal*/
|
||||
getLanguageServiceRefCounts(path: Path): [string, number | undefined][];
|
||||
|
||||
reportStats(): string;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
export interface ExternalDocumentCache {
|
||||
setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile): void;
|
||||
getDocument(key: DocumentRegistryBucketKey, path: Path): SourceFile | undefined;
|
||||
}
|
||||
|
||||
export type DocumentRegistryBucketKey = string & { __bucketKey: any };
|
||||
|
||||
interface DocumentRegistryEntry {
|
||||
|
@ -99,10 +108,14 @@ namespace ts {
|
|||
// language services are referencing the file, then the file can be removed from the
|
||||
// registry.
|
||||
languageServiceRefCount: number;
|
||||
owners: string[];
|
||||
}
|
||||
|
||||
export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory = ""): DocumentRegistry {
|
||||
export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry {
|
||||
return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory);
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boolean, currentDirectory = "", externalCache?: ExternalDocumentCache): DocumentRegistry {
|
||||
// Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have
|
||||
// for those settings.
|
||||
const buckets = createMap<Map<DocumentRegistryEntry>>();
|
||||
|
@ -123,12 +136,11 @@ namespace ts {
|
|||
function reportStats() {
|
||||
const bucketInfoArray = arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => {
|
||||
const entries = buckets.get(name);
|
||||
const sourceFiles: { name: string; refCount: number; references: string[]; }[] = [];
|
||||
const sourceFiles: { name: string; refCount: number; }[] = [];
|
||||
entries.forEach((entry, name) => {
|
||||
sourceFiles.push({
|
||||
name,
|
||||
refCount: entry.languageServiceRefCount,
|
||||
references: entry.owners.slice(0)
|
||||
refCount: entry.languageServiceRefCount
|
||||
});
|
||||
});
|
||||
sourceFiles.sort((x, y) => y.refCount - x.refCount);
|
||||
|
@ -173,14 +185,27 @@ namespace ts {
|
|||
const bucket = getBucketForCompilationSettings(key, /*createIfMissing*/ true);
|
||||
let entry = bucket.get(path);
|
||||
const scriptTarget = scriptKind === ScriptKind.JSON ? ScriptTarget.JSON : compilationSettings.target;
|
||||
if (!entry && externalCache) {
|
||||
const sourceFile = externalCache.getDocument(key, path);
|
||||
if (sourceFile) {
|
||||
Debug.assert(acquiring);
|
||||
entry = {
|
||||
sourceFile,
|
||||
languageServiceRefCount: 0
|
||||
};
|
||||
bucket.set(path, entry);
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry) {
|
||||
// Have never seen this file with these settings. Create a new source file for it.
|
||||
const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, scriptTarget, version, /*setNodeParents*/ false, scriptKind);
|
||||
|
||||
if (externalCache) {
|
||||
externalCache.setDocument(key, path, sourceFile);
|
||||
}
|
||||
entry = {
|
||||
sourceFile,
|
||||
languageServiceRefCount: 1,
|
||||
owners: []
|
||||
};
|
||||
bucket.set(path, entry);
|
||||
}
|
||||
|
@ -191,6 +216,9 @@ namespace ts {
|
|||
if (entry.sourceFile.version !== version) {
|
||||
entry.sourceFile = updateLanguageServiceSourceFile(entry.sourceFile, scriptSnapshot, version,
|
||||
scriptSnapshot.getChangeRange(entry.sourceFile.scriptSnapshot));
|
||||
if (externalCache) {
|
||||
externalCache.setDocument(key, path, entry.sourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're acquiring, then this is the first time this LS is asking for this document.
|
||||
|
@ -202,6 +230,7 @@ namespace ts {
|
|||
entry.languageServiceRefCount++;
|
||||
}
|
||||
}
|
||||
Debug.assert(entry.languageServiceRefCount !== 0);
|
||||
|
||||
return entry.sourceFile;
|
||||
}
|
||||
|
@ -225,6 +254,13 @@ namespace ts {
|
|||
}
|
||||
}
|
||||
|
||||
function getLanguageServiceRefCounts(path: Path) {
|
||||
return arrayFrom(buckets.entries(), ([key, bucket]): [string, number | undefined] => {
|
||||
const entry = bucket.get(path);
|
||||
return [key, entry && entry.languageServiceRefCount];
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
acquireDocument,
|
||||
acquireDocumentWithKey,
|
||||
|
@ -232,6 +268,7 @@ namespace ts {
|
|||
updateDocumentWithKey,
|
||||
releaseDocument,
|
||||
releaseDocumentWithKey,
|
||||
getLanguageServiceRefCounts,
|
||||
reportStats,
|
||||
getKeyForCompilationSettings
|
||||
};
|
||||
|
|
|
@ -8134,7 +8134,6 @@ declare namespace ts.server {
|
|||
syntaxOnly?: boolean;
|
||||
}
|
||||
class ProjectService {
|
||||
private readonly documentRegistry;
|
||||
/**
|
||||
* Container of all known scripts
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue