1namespace ts { 2 /** 3 * The document registry represents a store of SourceFile objects that can be shared between 4 * multiple LanguageService instances. A LanguageService instance holds on the SourceFile (AST) 5 * of files in the context. 6 * SourceFile objects account for most of the memory usage by the language service. Sharing 7 * the same DocumentRegistry instance between different instances of LanguageService allow 8 * for more efficient memory utilization since all projects will share at least the library 9 * file (lib.d.ts). 10 * 11 * A more advanced use of the document registry is to serialize sourceFile objects to disk 12 * and re-hydrate them when needed. 13 * 14 * To create a default DocumentRegistry, use createDocumentRegistry to create one, and pass it 15 * to all subsequent createLanguageService calls. 16 */ 17 export interface DocumentRegistry { 18 /** 19 * Request a stored SourceFile with a given fileName and compilationSettings. 20 * The first call to acquire will call createLanguageServiceSourceFile to generate 21 * the SourceFile if was not found in the registry. 22 * 23 * @param fileName The name of the file requested 24 * @param compilationSettings Some compilation settings like target affects the 25 * shape of a the resulting SourceFile. This allows the DocumentRegistry to store 26 * multiple copies of the same file for different compilation settings. 27 * @param scriptSnapshot Text of the file. Only used if the file was not found 28 * in the registry and a new one was created. 29 * @param version Current version of the file. Only used if the file was not found 30 * in the registry and a new one was created. 31 */ 32 acquireDocument( 33 fileName: string, 34 compilationSettings: CompilerOptions, 35 scriptSnapshot: IScriptSnapshot, 36 version: string, 37 scriptKind?: ScriptKind): SourceFile; 38 39 acquireDocumentWithKey( 40 fileName: string, 41 path: Path, 42 compilationSettings: CompilerOptions, 43 key: DocumentRegistryBucketKey, 44 scriptSnapshot: IScriptSnapshot, 45 version: string, 46 scriptKind?: ScriptKind): SourceFile; 47 48 /** 49 * Request an updated version of an already existing SourceFile with a given fileName 50 * and compilationSettings. The update will in-turn call updateLanguageServiceSourceFile 51 * to get an updated SourceFile. 52 * 53 * @param fileName The name of the file requested 54 * @param compilationSettings Some compilation settings like target affects the 55 * shape of a the resulting SourceFile. This allows the DocumentRegistry to store 56 * multiple copies of the same file for different compilation settings. 57 * @param scriptSnapshot Text of the file. 58 * @param version Current version of the file. 59 */ 60 updateDocument( 61 fileName: string, 62 compilationSettings: CompilerOptions, 63 scriptSnapshot: IScriptSnapshot, 64 version: string, 65 scriptKind?: ScriptKind): SourceFile; 66 67 updateDocumentWithKey( 68 fileName: string, 69 path: Path, 70 compilationSettings: CompilerOptions, 71 key: DocumentRegistryBucketKey, 72 scriptSnapshot: IScriptSnapshot, 73 version: string, 74 scriptKind?: ScriptKind): SourceFile; 75 76 getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey; 77 /** 78 * Informs the DocumentRegistry that a file is not needed any longer. 79 * 80 * Note: It is not allowed to call release on a SourceFile that was not acquired from 81 * this registry originally. 82 * 83 * @param fileName The name of the file to be released 84 * @param compilationSettings The compilation settings used to acquire the file 85 */ 86 releaseDocument(fileName: string, compilationSettings: CompilerOptions): void; 87 88 releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void; 89 90 /*@internal*/ 91 getLanguageServiceRefCounts(path: Path): [string, number | undefined][]; 92 93 reportStats(): string; 94 } 95 96 /*@internal*/ 97 export interface ExternalDocumentCache { 98 setDocument(key: DocumentRegistryBucketKey, path: Path, sourceFile: SourceFile): void; 99 getDocument(key: DocumentRegistryBucketKey, path: Path): SourceFile | undefined; 100 } 101 102 export type DocumentRegistryBucketKey = string & { __bucketKey: any }; 103 104 interface DocumentRegistryEntry { 105 sourceFile: SourceFile; 106 107 // The number of language services that this source file is referenced in. When no more 108 // language services are referencing the file, then the file can be removed from the 109 // registry. 110 languageServiceRefCount: number; 111 } 112 113 export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry { 114 return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory); 115 } 116 117 /*@internal*/ 118 export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boolean, currentDirectory = "", externalCache?: ExternalDocumentCache): DocumentRegistry { 119 // Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have 120 // for those settings. 121 const buckets = new Map<string, ESMap<Path, DocumentRegistryEntry>>(); 122 const getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames); 123 124 function reportStats() { 125 const bucketInfoArray = arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => { 126 const entries = buckets.get(name)!; 127 const sourceFiles: { name: string; refCount: number; }[] = []; 128 entries.forEach((entry, name) => { 129 sourceFiles.push({ 130 name, 131 refCount: entry.languageServiceRefCount 132 }); 133 }); 134 sourceFiles.sort((x, y) => y.refCount - x.refCount); 135 return { 136 bucket: name, 137 sourceFiles 138 }; 139 }); 140 return JSON.stringify(bucketInfoArray, undefined, 2); 141 } 142 143 function acquireDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { 144 const path = toPath(fileName, currentDirectory, getCanonicalFileName); 145 const key = getKeyForCompilationSettings(compilationSettings); 146 return acquireDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); 147 } 148 149 function acquireDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { 150 return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ true, scriptKind); 151 } 152 153 function updateDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { 154 const path = toPath(fileName, currentDirectory, getCanonicalFileName); 155 const key = getKeyForCompilationSettings(compilationSettings); 156 return updateDocumentWithKey(fileName, path, compilationSettings, key, scriptSnapshot, version, scriptKind); 157 } 158 159 function updateDocumentWithKey(fileName: string, path: Path, compilationSettings: CompilerOptions, key: DocumentRegistryBucketKey, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { 160 return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ false, scriptKind); 161 } 162 163 function acquireOrUpdateDocument( 164 fileName: string, 165 path: Path, 166 compilationSettings: CompilerOptions, 167 key: DocumentRegistryBucketKey, 168 scriptSnapshot: IScriptSnapshot, 169 version: string, 170 acquiring: boolean, 171 scriptKind?: ScriptKind): SourceFile { 172 173 const bucket = getOrUpdate(buckets, key, () => new Map<Path, DocumentRegistryEntry>()); 174 let entry = bucket.get(path); 175 const scriptTarget = scriptKind === ScriptKind.JSON ? ScriptTarget.JSON : compilationSettings.target || ScriptTarget.ES5; 176 if (!entry && externalCache) { 177 const sourceFile = externalCache.getDocument(key, path); 178 if (sourceFile) { 179 Debug.assert(acquiring); 180 entry = { 181 sourceFile, 182 languageServiceRefCount: 0 183 }; 184 bucket.set(path, entry); 185 } 186 } 187 188 if (!entry) { 189 // Have never seen this file with these settings. Create a new source file for it. 190 const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, scriptTarget, version, /*setNodeParents*/ false, scriptKind, compilationSettings); 191 if (externalCache) { 192 externalCache.setDocument(key, path, sourceFile); 193 } 194 entry = { 195 sourceFile, 196 languageServiceRefCount: 1, 197 }; 198 bucket.set(path, entry); 199 } 200 else { 201 // We have an entry for this file. However, it may be for a different version of 202 // the script snapshot. If so, update it appropriately. Otherwise, we can just 203 // return it as is. 204 if (entry.sourceFile.version !== version) { 205 entry.sourceFile = updateLanguageServiceSourceFile(entry.sourceFile, scriptSnapshot, version, 206 scriptSnapshot.getChangeRange(entry.sourceFile.scriptSnapshot!), /*aggressiveChecks*/ undefined, compilationSettings); // TODO: GH#18217 207 if (externalCache) { 208 externalCache.setDocument(key, path, entry.sourceFile); 209 } 210 } 211 212 // If we're acquiring, then this is the first time this LS is asking for this document. 213 // Increase our ref count so we know there's another LS using the document. If we're 214 // not acquiring, then that means the LS is 'updating' the file instead, and that means 215 // it has already acquired the document previously. As such, we do not need to increase 216 // the ref count. 217 if (acquiring) { 218 entry.languageServiceRefCount++; 219 } 220 } 221 Debug.assert(entry.languageServiceRefCount !== 0); 222 223 return entry.sourceFile; 224 } 225 226 function releaseDocument(fileName: string, compilationSettings: CompilerOptions): void { 227 const path = toPath(fileName, currentDirectory, getCanonicalFileName); 228 const key = getKeyForCompilationSettings(compilationSettings); 229 return releaseDocumentWithKey(path, key); 230 } 231 232 function releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void { 233 const bucket = Debug.checkDefined(buckets.get(key)); 234 const entry = bucket.get(path)!; 235 entry.languageServiceRefCount--; 236 237 Debug.assert(entry.languageServiceRefCount >= 0); 238 if (entry.languageServiceRefCount === 0) { 239 bucket.delete(path); 240 } 241 } 242 243 function getLanguageServiceRefCounts(path: Path) { 244 return arrayFrom(buckets.entries(), ([key, bucket]): [string, number | undefined] => { 245 const entry = bucket.get(path); 246 return [key, entry && entry.languageServiceRefCount]; 247 }); 248 } 249 250 return { 251 acquireDocument, 252 acquireDocumentWithKey, 253 updateDocument, 254 updateDocumentWithKey, 255 releaseDocument, 256 releaseDocumentWithKey, 257 getLanguageServiceRefCounts, 258 reportStats, 259 getKeyForCompilationSettings 260 }; 261 } 262 263 function getKeyForCompilationSettings(settings: CompilerOptions): DocumentRegistryBucketKey { 264 return sourceFileAffectingCompilerOptions.map(option => getCompilerOptionValue(settings, option)).join("|") as DocumentRegistryBucketKey; 265 } 266} 267