From 801367905d013b1d6b58f3033e03352f1c31b3e5 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 12 Apr 2024 16:29:35 +0900 Subject: [PATCH] perf(typescript): fix v2 tsc performance regression close https://github.com/vuejs/language-tools/issues/4238 --- .../typescript/lib/node/proxyCreateProgram.ts | 190 +++++++++++------- 1 file changed, 122 insertions(+), 68 deletions(-) diff --git a/packages/typescript/lib/node/proxyCreateProgram.ts b/packages/typescript/lib/node/proxyCreateProgram.ts index b3f72f11..f27957a3 100644 --- a/packages/typescript/lib/node/proxyCreateProgram.ts +++ b/packages/typescript/lib/node/proxyCreateProgram.ts @@ -1,58 +1,93 @@ -import { LanguagePlugin, createLanguage } from '@volar/language-core'; +import { Language, LanguagePlugin, createLanguage } from '@volar/language-core'; import type * as ts from 'typescript'; import { createResolveModuleName } from '../resolveModuleName'; import { decorateProgram } from './decorateProgram'; +const arrayEqual = (a: readonly any[], b: readonly any[]) => { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +}; +const objectEqual = (a: any, b: any) => { + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + for (const key of keysA) { + if (a[key] !== b[key]) return false; + } + return true; +}; + export function proxyCreateProgram( ts: typeof import('typescript'), original: typeof ts['createProgram'], getLanguagePlugins: (ts: typeof import('typescript'), options: ts.CreateProgramOptions) => LanguagePlugin[], getLanguageId: (fileName: string) => string, ) { + const sourceFileSnapshots = new Map(); + const parsedSourceFiles = new WeakMap(); + + let lastOptions: ts.CreateProgramOptions | undefined; + let languagePlugins: LanguagePlugin[] | undefined; + let language: Language | undefined; + let moduleResolutionCache: ts.ModuleResolutionCache; + return new Proxy(original, { apply: (target, thisArg, args) => { const options = args[0] as ts.CreateProgramOptions; assert(!!options.host, '!!options.host'); - const languagePlugins = getLanguagePlugins(ts, options); - const extensions = languagePlugins - .map(plugin => plugin.typescript?.extraFileExtensions.map(({ extension }) => `.${extension}`) ?? []) - .flat(); - const sourceFileToSnapshotMap = new WeakMap(); - const language = createLanguage( - languagePlugins, - ts.sys.useCaseSensitiveFileNames, - fileName => { - let snapshot: ts.IScriptSnapshot | undefined; - const sourceFile = originalHost.getSourceFile(fileName, 99 satisfies ts.ScriptTarget.ESNext); - if (sourceFile) { - snapshot = sourceFileToSnapshotMap.get(sourceFile); - if (!snapshot) { - snapshot = { - getChangeRange() { - return undefined; - }, - getLength() { - return sourceFile.text.length; - }, - getText(start, end) { - return sourceFile.text.substring(start, end); - }, - }; - sourceFileToSnapshotMap.set(sourceFile, snapshot); + if ( + !lastOptions + || !languagePlugins + || !language + || !arrayEqual(options.rootNames, lastOptions.rootNames) + || !objectEqual(options.options, lastOptions.options) + ) { + moduleResolutionCache = ts.createModuleResolutionCache(options.host.getCurrentDirectory(), options.host.getCanonicalFileName, options.options); + lastOptions = options; + languagePlugins = getLanguagePlugins(ts, options); + language = createLanguage( + languagePlugins, + ts.sys.useCaseSensitiveFileNames, + fileName => { + if (!sourceFileSnapshots.has(fileName)) { + const sourceFileText = originalHost.readFile(fileName); + if (sourceFileText !== undefined) { + sourceFileSnapshots.set(fileName, [undefined, { + getChangeRange() { + return undefined; + }, + getLength() { + return sourceFileText.length; + }, + getText(start, end) { + return sourceFileText.substring(start, end); + }, + }]); + } + else { + sourceFileSnapshots.set(fileName, [undefined, undefined]); + } + } + const snapshot = sourceFileSnapshots.get(fileName)?.[1]; + if (snapshot) { + language!.scripts.set(fileName, getLanguageId(fileName), snapshot); + } + else { + language!.scripts.delete(fileName); } } - if (snapshot) { - language.scripts.set(fileName, getLanguageId(fileName), snapshot); - } - else { - language.scripts.delete(fileName); - } - } - ); - const parsedSourceFiles = new WeakMap(); + ); + } + const originalHost = options.host; + const extensions = languagePlugins + .map(plugin => plugin.typescript?.extraFileExtensions.map(({ extension }) => `.${extension}`) ?? []) + .flat(); options.host = { ...originalHost }; options.host.getSourceFile = ( @@ -62,48 +97,67 @@ export function proxyCreateProgram( shouldCreateNewSourceFile, ) => { const originalSourceFile = originalHost.getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile); - if (originalSourceFile && extensions.some(ext => fileName.endsWith(ext))) { - let sourceFile2 = parsedSourceFiles.get(originalSourceFile); - if (!sourceFile2) { - const sourceScript = language.scripts.get(fileName); - assert(!!sourceScript, '!!sourceScript'); - let patchedText = originalSourceFile.text.split('\n').map(line => ' '.repeat(line.length)).join('\n'); - let scriptKind = ts.ScriptKind.TS; - if (sourceScript.generated?.languagePlugin.typescript) { - const { getServiceScript, getExtraServiceScripts } = sourceScript.generated.languagePlugin.typescript; - const serviceScript = getServiceScript(sourceScript.generated.root); - if (serviceScript) { - scriptKind = serviceScript.scriptKind; - patchedText += serviceScript.code.snapshot.getText(0, serviceScript.code.snapshot.getLength()); - } - if (getExtraServiceScripts) { - console.warn('getExtraServiceScripts() is not available in this use case.'); - } + if ( + !sourceFileSnapshots.has(fileName) + || sourceFileSnapshots.get(fileName)?.[0] !== originalSourceFile + ) { + if (originalSourceFile) { + sourceFileSnapshots.set(fileName, [originalSourceFile, { + getChangeRange() { + return undefined; + }, + getLength() { + return originalSourceFile.text.length; + }, + getText(start, end) { + return originalSourceFile.text.substring(start, end); + }, + }]); + } + else { + sourceFileSnapshots.set(fileName, [undefined, undefined]); + } + } + if (!originalSourceFile) { + return; + } + if (!parsedSourceFiles.has(originalSourceFile)) { + const sourceScript = language!.scripts.get(fileName); + assert(!!sourceScript, '!!sourceScript'); + parsedSourceFiles.set(originalSourceFile, originalSourceFile); + if (sourceScript.generated?.languagePlugin.typescript) { + const { getServiceScript, getExtraServiceScripts } = sourceScript.generated.languagePlugin.typescript; + const serviceScript = getServiceScript(sourceScript.generated.root); + if (serviceScript) { + let patchedText = originalSourceFile.text.split('\n').map(line => ' '.repeat(line.length)).join('\n'); + let scriptKind = ts.ScriptKind.TS; + scriptKind = serviceScript.scriptKind; + patchedText += serviceScript.code.snapshot.getText(0, serviceScript.code.snapshot.getLength()); + const parsedSourceFile = ts.createSourceFile( + fileName, + patchedText, + languageVersionOrOptions, + undefined, + scriptKind, + ); + // @ts-expect-error + parsedSourceFile.version = originalSourceFile.version; + parsedSourceFiles.set(originalSourceFile, parsedSourceFile); + } + if (getExtraServiceScripts) { + console.warn('getExtraServiceScripts() is not available in this use case.'); } - sourceFile2 = ts.createSourceFile( - fileName, - patchedText, - 99 satisfies ts.ScriptTarget.ESNext, - true, - scriptKind, - ); - // @ts-expect-error - sourceFile2.version = originalSourceFile.version; - parsedSourceFiles.set(originalSourceFile, sourceFile2); } - return sourceFile2; } - - return originalSourceFile; + return parsedSourceFiles.get(originalSourceFile); }; if (extensions.length) { options.options.allowArbitraryExtensions = true; - const resolveModuleName = createResolveModuleName(ts, originalHost, language.plugins, fileName => language.scripts.get(fileName)); + const resolveModuleName = createResolveModuleName(ts, originalHost, language.plugins, fileName => language!.scripts.get(fileName)); const resolveModuleNameLiterals = originalHost.resolveModuleNameLiterals; const resolveModuleNames = originalHost.resolveModuleNames; - const moduleResolutionCache = ts.createModuleResolutionCache(originalHost.getCurrentDirectory(), originalHost.getCanonicalFileName, options.options); options.host.resolveModuleNameLiterals = ( moduleLiterals,