Repository: microsoft/vscode (TypeScript)
Date: 2022-05-25 19:31:23+00:00
Task type: Bug Fix

Issue:
Many links on a wrapped line in the terminal causes performance issues
![image](https://user-images.githubusercontent.com/2193314/170327473-f603b43d-fc97-452d-8d9b-60f9ece8ec42.png)

When mousing over the terminal:

![image](https://user-images.githubusercontent.com/2193314/170327538-e6b31120-56d8-49fd-84ce-53f2a169fbc3.png)



Issue discussion:

Solution patch:
diff --git a/src/vs/workbench/contrib/terminal/browser/links/links.ts b/src/vs/workbench/contrib/terminal/browser/links/links.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/links.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/links.ts
@@ -17,6 +17,12 @@ export interface ITerminalLinkDetector {
 	 */
 	readonly xterm: Terminal;
 
+	/**
+	 * The maximum link length possible for this detector, this puts a cap on how much of a wrapped
+	 * line to consider to prevent performance problems.
+	 */
+	readonly maxLinkLength: number;
+
 	/**
 	 * Detects links within the _wrapped_ line range provided and returns them as an array.
 	 *
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkDetector.ts
@@ -8,14 +8,9 @@ import { convertLinkRangeToBuffer, getXtermLineContent } from 'vs/workbench/cont
 import { ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal';
 import { IBufferLine, Terminal } from 'xterm';
 
-const enum Constants {
-	/**
-	 * The max line length to try extract word links from.
-	 */
-	MaxLineLength = 2000
-}
-
 export class TerminalExternalLinkDetector implements ITerminalLinkDetector {
+	readonly maxLinkLength = 2000;
+
 	constructor(
 		readonly id: string,
 		readonly xterm: Terminal,
@@ -26,7 +21,7 @@ export class TerminalExternalLinkDetector implements ITerminalLinkDetector {
 	async detect(lines: IBufferLine[], startLine: number, endLine: number): Promise<ITerminalSimpleLink[]> {
 		// Get the text representation of the wrapped line
 		const text = getXtermLineContent(this.xterm.buffer.active, startLine, endLine, this.xterm.cols);
-		if (text === '' || text.length > Constants.MaxLineLength) {
+		if (text === '' || text.length > this.maxLinkLength) {
 			return [];
 		}
 
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkDetectorAdapter.ts
@@ -59,12 +59,19 @@ export class TerminalLinkDetectorAdapter extends Disposable implements ILinkProv
 			this._detector.xterm.buffer.active.getLine(startLine)!
 		];
 
-		while (startLine >= 0 && this._detector.xterm.buffer.active.getLine(startLine)?.isWrapped) {
+		// Cap the maximum context on either side of the line being provided, by taking the context
+		// around the line being provided for this ensures the line the pointer is on will have
+		// links provided.
+		const maxLineContext = Math.max(this._detector.maxLinkLength / this._detector.xterm.cols);
+		const minStartLine = Math.max(startLine - maxLineContext, 0);
+		const maxEndLine = Math.min(endLine + maxLineContext, this._detector.xterm.buffer.active.length);
+
+		while (startLine >= minStartLine && this._detector.xterm.buffer.active.getLine(startLine)?.isWrapped) {
 			lines.unshift(this._detector.xterm.buffer.active.getLine(startLine - 1)!);
 			startLine--;
 		}
 
-		while (endLine < this._detector.xterm.buffer.active.length && this._detector.xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
+		while (endLine < maxEndLine && this._detector.xterm.buffer.active.getLine(endLine + 1)?.isWrapped) {
 			lines.push(this._detector.xterm.buffer.active.getLine(endLine + 1)!);
 			endLine++;
 		}
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts
@@ -122,6 +122,10 @@ export function convertBufferRangeToViewport(bufferRange: IBufferRange, viewport
 }
 
 export function getXtermLineContent(buffer: IBuffer, lineStart: number, lineEnd: number, cols: number): string {
+	// Cap the maximum number of lines generated to prevent potential performance problems. This is
+	// more of a sanity check as the wrapped line should already be trimmed down at this point.
+	const maxLineLength = Math.max(2048 / cols * 2);
+	lineEnd = Math.min(lineEnd, lineStart + maxLineLength);
 	let content = '';
 	for (let i = lineStart; i <= lineEnd; i++) {
 		// Make sure only 0 to cols are considered as resizing when windows mode is enabled will
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLocalLinkDetector.ts
@@ -67,6 +67,12 @@ export const lineAndColumnClauseGroupCount = 6;
 export class TerminalLocalLinkDetector implements ITerminalLinkDetector {
 	static id = 'local';
 
+	// This was chosen as a reasonable maximum line length given the tradeoff between performance
+	// and how likely it is to encounter such a large line length. Some useful reference points:
+	// - Window old max length: 260 ($MAX_PATH)
+	// - Linux max length: 4096 ($PATH_MAX)
+	readonly maxLinkLength = 500;
+
 	constructor(
 		readonly xterm: Terminal,
 		private readonly _capabilities: ITerminalCapabilityStore,
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalUriLinkDetector.ts
@@ -17,18 +17,15 @@ const enum Constants {
 	 * The maximum number of links in a line to resolve against the file system. This limit is put
 	 * in place to avoid sending excessive data when remote connections are in place.
 	 */
-	MaxResolvedLinksInLine = 10,
-
-	/**
-	 * The maximum length of a link to resolve against the file system. This limit is put in place
-	 * to avoid sending excessive data when remote connections are in place.
-	 */
-	MaxResolvedLinkLength = 1024,
+	MaxResolvedLinksInLine = 10
 }
 
 export class TerminalUriLinkDetector implements ITerminalLinkDetector {
 	static id = 'uri';
 
+	// 2048 is the maximum URL length
+	readonly maxLinkLength = 2048;
+
 	constructor(
 		readonly xterm: Terminal,
 		private readonly _resolvePath: (link: string, uri?: URI) => Promise<ResolvedLink>,
@@ -58,6 +55,11 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector {
 
 			const text = computedLink.url?.toString() || '';
 
+			// Don't try resolve any links of excessive length
+			if (text.length > this.maxLinkLength) {
+				continue;
+			}
+
 			// Handle non-file scheme links
 			if (uri.scheme !== Schemas.file) {
 				links.push({
@@ -69,11 +71,6 @@ export class TerminalUriLinkDetector implements ITerminalLinkDetector {
 				continue;
 			}
 
-			// Don't try resolve any links of excessive length
-			if (text.length > Constants.MaxResolvedLinkLength) {
-				continue;
-			}
-
 			// Filter out URI with unrecognized authorities
 			if (uri.authority.length !== 2 && uri.authority.endsWith(':')) {
 				continue;
diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts
--- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts
+++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkDetector.ts
@@ -25,6 +25,10 @@ interface Word {
 export class TerminalWordLinkDetector implements ITerminalLinkDetector {
 	static id = 'word';
 
+	// Word links typically search the workspace so it makes sense that their maximum link length is
+	// quite small.
+	readonly maxLinkLength = 100;
+
 	constructor(
 		readonly xterm: Terminal,
 		@IConfigurationService private readonly _configurationService: IConfigurationService,


Test patch:
diff --git a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts
--- a/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts
+++ b/src/vs/workbench/contrib/terminal/test/browser/links/terminalUriLinkDetector.test.ts
@@ -79,25 +79,22 @@ suite('Workbench - TerminalUriLinkDetector', () => {
 			{ range: [[16, 1], [29, 1]], text: 'http://bar.foo' }
 		]);
 	});
-	test('should not filtrer out https:// link that exceed 1024 characters', async () => {
-		// 8 + 101 * 10 = 1018 characters
-		await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(101)}`, [{
-			range: [[1, 1], [58, 13]],
-			text: `https://${'foobarbaz/'.repeat(101)}`
-		}]);
-		// 8 + 102 * 10 = 1028 characters
-		await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(102)}`, [{
-			range: [[1, 1], [68, 13]],
-			text: `https://${'foobarbaz/'.repeat(102)}`
+	test('should filter out https:// link that exceed 4096 characters', async () => {
+		// 8 + 200 * 10 = 2008 characters
+		await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(200)}`, [{
+			range: [[1, 1], [8, 26]],
+			text: `https://${'foobarbaz/'.repeat(200)}`
 		}]);
+		// 8 + 450 * 10 = 4508 characters
+		await assertLink(TerminalBuiltinLinkType.Url, `https://${'foobarbaz/'.repeat(450)}`, []);
 	});
-	test('should filter out file:// links that exceed 1024 characters', async () => {
-		// 8 + 101 * 10 = 1018 characters
-		await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(101)}`, [{
-			text: `file:///${'foobarbaz/'.repeat(101)}`,
-			range: [[1, 1], [58, 13]]
+	test('should filter out file:// links that exceed 4096 characters', async () => {
+		// 8 + 200 * 10 = 2008 characters
+		await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(200)}`, [{
+			text: `file:///${'foobarbaz/'.repeat(200)}`,
+			range: [[1, 1], [8, 26]]
 		}]);
-		// 8 + 102 * 10 = 1028 characters
-		await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(102)}`, []);
+		// 8 + 450 * 10 = 4508 characters
+		await assertLink(TerminalBuiltinLinkType.LocalFile, `file:///${'foobarbaz/'.repeat(450)}`, []);
 	});
 });