Skip to content

Commit

Permalink
(perf) typescript language service size limit (#1194)
Browse files Browse the repository at this point in the history
#965
Add a size limit for typescript language service similar to the one in the tsserver. When the size limit exceeds, ts language service goes into a reduced mode. Project-wide IntelliSense is disabled. It would only use the files opened in the client. Kind of like when you don't have a tsconfig.json file. When this happens a warning message will show up.

Note if you have a really big project and you have excluded everything needed but it still exceeds this limit, you can disable it with the disableSizeLimit compiler options in the tsconfig/jsconfig.
  • Loading branch information
jasonlyu123 authored Oct 8, 2021
1 parent 4450608 commit 898555d
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ export class LSAndTSDocResolver {
* @param docManager
* @param workspaceUris
* @param configManager
* @param notifyExceedSizeLimit
* @param isSvelteCheck True, if used in the context of svelte-check
* @param tsconfigPath This should only be set via svelte-check. Makes sure all documents are resolved to that tsconfig. Has to be absolute.
*/
constructor(
private readonly docManager: DocumentManager,
private readonly workspaceUris: string[],
private readonly configManager: LSConfigManager,
private readonly notifyExceedSizeLimit?: () => void,
private readonly isSvelteCheck = false,
private readonly tsconfigPath?: string
) {
Expand Down Expand Up @@ -69,7 +71,8 @@ export class LSAndTSDocResolver {
ambientTypesSource: this.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
createDocument: this.createDocument,
transformOnTemplateError: !this.isSvelteCheck,
globalSnapshotsManager: this.globalSnapshotsManager
globalSnapshotsManager: this.globalSnapshotsManager,
notifyExceedSizeLimit: this.notifyExceedSizeLimit
};
}

Expand Down
83 changes: 81 additions & 2 deletions packages/language-server/src/plugins/typescript/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ignoredBuildDirectories,
SnapshotManager
} from './SnapshotManager';
import { ensureRealSvelteFilePath, findTsConfigPath } from './utils';
import { ensureRealSvelteFilePath, findTsConfigPath, hasTsExtensions } from './utils';

export interface LanguageServiceContainer {
readonly tsconfigPath: string;
Expand All @@ -39,13 +39,16 @@ export interface LanguageServiceContainer {
fileBelongsToProject(filePath: string): boolean;
}

const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; // 20 MB
const services = new Map<string, Promise<LanguageServiceContainer>>();
const serviceSizeMap: Map<string, number> = new Map();

export interface LanguageServiceDocumentContext {
ambientTypesSource: string;
transformOnTemplateError: boolean;
createDocument: (fileName: string, content: string) => Document;
globalSnapshotsManager: GlobalSnapshotsManager;
notifyExceedSizeLimit: (() => void) | undefined;
}

export async function getService(
Expand Down Expand Up @@ -123,12 +126,14 @@ async function createLanguageService(
'./svelte-native-jsx.d.ts'
].map((f) => ts.sys.resolvePath(resolve(svelteTsPath, f)));

let languageServiceReducedMode = false;

const host: ts.LanguageServiceHost = {
getCompilationSettings: () => compilerOptions,
getScriptFileNames: () =>
Array.from(
new Set([
...snapshotManager.getProjectFileNames(),
...(languageServiceReducedMode ? [] : snapshotManager.getProjectFileNames()),
...snapshotManager.getFileNames(),
...svelteTsxFiles
])
Expand All @@ -150,6 +155,8 @@ async function createLanguageService(
transformOnTemplateError: docContext.transformOnTemplateError
};

reduceLanguageServiceCapabilityIfFileSizeTooBig();

return {
tsconfigPath,
compilerOptions,
Expand Down Expand Up @@ -232,7 +239,15 @@ async function createLanguageService(
}

function updateProjectFiles(): void {
const projectFileCountBefore = snapshotManager.getProjectFileNames().length;
snapshotManager.updateProjectFiles();
const projectFileCountAfter = snapshotManager.getProjectFileNames().length;

if (projectFileCountAfter <= projectFileCountBefore) {
return;
}

reduceLanguageServiceCapabilityIfFileSizeTooBig();
}

function hasFile(filePath: string): boolean {
Expand Down Expand Up @@ -350,4 +365,68 @@ async function createLanguageService(
function getDefaultExclude() {
return ['node_modules', ...ignoredBuildDirectories];
}

/**
* Disable usage of project files.
* running language service in a reduced mode for
* large projects with improperly excluded tsconfig.
*/
function reduceLanguageServiceCapabilityIfFileSizeTooBig() {
if (exceedsTotalSizeLimitForNonTsFiles(compilerOptions, tsconfigPath, snapshotManager)) {
languageService.cleanupSemanticCache();
languageServiceReducedMode = true;
docContext.notifyExceedSizeLimit?.();
}
}
}

/**
* adopted from https://github.com/microsoft/TypeScript/blob/3c8e45b304b8572094c5d7fbb9cd768dbf6417c0/src/server/editorServices.ts#L1955
*/
function exceedsTotalSizeLimitForNonTsFiles(
compilerOptions: ts.CompilerOptions,
tsconfigPath: string,
snapshotManager: SnapshotManager
): boolean {
if (compilerOptions.disableSizeLimit) {
return false;
}

let availableSpace = maxProgramSizeForNonTsFiles;
serviceSizeMap.set(tsconfigPath, 0);

serviceSizeMap.forEach((size) => {
availableSpace -= size;
});

let totalNonTsFileSize = 0;

const fileNames = snapshotManager.getProjectFileNames();
for (const fileName of fileNames) {
if (hasTsExtensions(fileName)) {
continue;
}

totalNonTsFileSize += ts.sys.getFileSize?.(fileName) ?? 0;

if (totalNonTsFileSize > availableSpace) {
const top5LargestFiles = fileNames
.filter((name) => !hasTsExtensions(name))
.map((name) => ({ name, size: ts.sys.getFileSize?.(name) ?? 0 }))
.sort((a, b) => b.size - a.size)
.slice(0, 5);

Logger.log(
`Non TS file size exceeded limit (${totalNonTsFileSize}). ` +
`Largest files: ${top5LargestFiles
.map((file) => `${file.name}:${file.size}`)
.join(', ')}`
);

return true;
}
}

serviceSizeMap.set(tsconfigPath, totalNonTsFileSize);
return false;
}
8 changes: 8 additions & 0 deletions packages/language-server/src/plugins/typescript/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,11 @@ export function getDiagnosticTag(diagnostic: ts.Diagnostic): DiagnosticTag[] {
}
return tags;
}

export function hasTsExtensions(fileName: string) {
return (
fileName.endsWith(ts.Extension.Dts) ||
fileName.endsWith(ts.Extension.Tsx) ||
fileName.endsWith(ts.Extension.Ts)
);
}
17 changes: 16 additions & 1 deletion packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,12 @@ export function startServer(options?: LSOptions) {
pluginHost.register(
new TypeScriptPlugin(
configManager,
new LSAndTSDocResolver(docManager, workspaceUris.map(normalizeUri), configManager)
new LSAndTSDocResolver(
docManager,
workspaceUris.map(normalizeUri),
configManager,
notifyTsServiceExceedSizeLimit
)
)
);

Expand Down Expand Up @@ -241,6 +246,16 @@ export function startServer(options?: LSOptions) {
};
});

function notifyTsServiceExceedSizeLimit() {
connection?.sendNotification(ShowMessageNotification.type, {
message:
'Svelte language server detected a large amount of JS/Svelte files. ' +
'To enable project-wide JavaScript/TypeScript language features for Svelte files,' +
'exclude large folders in the tsconfig.json or jsconfig.json with source files that you do not work on.',
type: MessageType.Warning
});
}

connection.onExit(() => {
watcher?.dispose();
});
Expand Down
1 change: 1 addition & 0 deletions packages/language-server/src/svelte-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class SvelteCheck {
this.docManager,
[pathToUrl(workspacePath)],
this.configManager,
undefined,
true,
options.tsconfig
);
Expand Down

0 comments on commit 898555d

Please sign in to comment.