Investigate detecting slow network connections (#178553) (#182653)

This commit is contained in:
Benjamin Pasero 2023-05-22 14:38:13 +02:00 committed by GitHub
parent 09f80f4c54
commit 487a08afe5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 322 additions and 149 deletions

View file

@ -322,10 +322,6 @@
"name": "vs/workbench/contrib/bracketPairColorizer2Telemetry",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/offline",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/contrib/remoteTunnel",
"project": "vscode-workbench"

View file

@ -95,6 +95,11 @@
"title": "Pause Connection (Test Reconnect)",
"category": "Remote-TestResolver",
"command": "vscode-testresolver.toggleConnectionPause"
},
{
"title": "Slowdown Connection (Test Slow Down Indicator)",
"category": "Remote-TestResolver",
"command": "vscode-testresolver.toggleConnectionSlowdown"
}
],
"menus": {

View file

@ -22,11 +22,25 @@ const enum CharCode {
let outputChannel: vscode.OutputChannel;
const SLOWED_DOWN_CONNECTION_DELAY = 800;
export function activate(context: vscode.ExtensionContext) {
let connectionPaused = false;
const connectionPausedEvent = new vscode.EventEmitter<boolean>();
let connectionSlowedDown = false;
const connectionSlowedDownEvent = new vscode.EventEmitter<boolean>();
const slowedDownConnections = new Set<Function>();
connectionSlowedDownEvent.event(slowed => {
if (!slowed) {
for (const cb of slowedDownConnections) {
cb();
}
slowedDownConnections.clear();
}
});
function getTunnelFeatures(): vscode.TunnelInformation['tunnelFeatures'] {
return {
elevation: true,
@ -50,6 +64,22 @@ export function activate(context: vscode.ExtensionContext) {
};
}
function maybeSlowdown(): Promise<void> | void {
if (connectionSlowedDown) {
return new Promise(resolve => {
const handle = setTimeout(() => {
resolve();
slowedDownConnections.delete(resolve);
}, SLOWED_DOWN_CONNECTION_DELAY);
slowedDownConnections.add(() => {
resolve();
clearTimeout(handle);
});
});
}
}
function doResolve(authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<vscode.ResolverResult> {
if (connectionPaused) {
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable('Not available right now');
@ -237,13 +267,15 @@ export function activate(context: vscode.ExtensionContext) {
connectionPausedEvent.event(_ => handleConnectionPause());
handleConnectionPause();
proxySocket.on('data', (data) => {
proxySocket.on('data', async (data) => {
await maybeSlowdown();
remoteReady = remoteSocket.write(data);
if (!remoteReady) {
proxySocket.pause();
}
});
remoteSocket.on('data', (data) => {
remoteSocket.on('data', async (data) => {
await maybeSlowdown();
localReady = proxySocket.write(data);
if (!localReady) {
remoteSocket.pause();
@ -358,6 +390,22 @@ export function activate(context: vscode.ExtensionContext) {
connectionPausedEvent.fire(connectionPaused);
}));
const slowdownStatusBarEntry = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
slowdownStatusBarEntry.text = 'Remote connection slowed down. Click to undo';
slowdownStatusBarEntry.command = 'vscode-testresolver.toggleConnectionSlowdown';
slowdownStatusBarEntry.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.toggleConnectionSlowdown', () => {
if (!connectionSlowedDown) {
connectionSlowedDown = true;
slowdownStatusBarEntry.show();
} else {
connectionSlowedDown = false;
slowdownStatusBarEntry.hide();
}
connectionSlowedDownEvent.fire(connectionSlowedDown);
}));
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.openTunnel', async () => {
const result = await vscode.window.showInputBox({
prompt: 'Enter the remote port for the tunnel',

View file

@ -500,11 +500,10 @@ function doCreateUri(path: string, queryValues: Map<string, string>): URI {
// Create workbench
create(document.body, {
...config,
settingsSyncOptions: config.settingsSyncOptions ? {
enabled: config.settingsSyncOptions.enabled,
} : undefined,
windowIndicator: config.windowIndicator ?? { label: '$(remote)', tooltip: `${product.nameShort} Web` },
settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined,
workspaceProvider: WorkspaceProvider.create(config),
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute),
credentialsProvider: config.remoteAuthority ? undefined : new LocalStorageCredentialsProvider() // with a remote, we don't use a local credentials provider
credentialsProvider: config.remoteAuthority ? undefined /* with a remote, we don't use a local credentials provider */ : new LocalStorageCredentialsProvider()
});
})();

View file

@ -991,7 +991,7 @@ export class IssueReporter extends Disposable {
const remoteDataTable = $('table', undefined,
$('tr', undefined,
$('td', undefined, 'Remote'),
$('td', undefined, remote.hostName)
$('td', undefined, remote.latency ? `${remote.hostName} (latency: ${remote.latency.current.toFixed(2)}ms last, ${remote.latency.average.toFixed(2)}ms average)` : remote.hostName)
),
$('tr', undefined,
$('td', undefined, 'OS'),

View file

@ -185,7 +185,7 @@ ${this.getInfos()}
|Item|Value|
|---|---|
|Remote|${remote.hostName}|
|Remote|${remote.latency ? `${remote.hostName} (latency: ${remote.latency.current.toFixed(2)}ms last, ${remote.latency.average.toFixed(2)}ms average)` : remote.hostName}|
|OS|${remote.machineInfo.os}|
|CPUs|${remote.machineInfo.cpus}|
|Memory (System)|${remote.machineInfo.memory}|

View file

@ -52,6 +52,10 @@ export interface SystemInfo extends IMachineInfo {
export interface IRemoteDiagnosticInfo extends IDiagnosticInfo {
hostName: string;
latency?: {
current: number;
average: number;
};
}
export interface IRemoteDiagnosticError {

View file

@ -1,97 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { STATUS_BAR_FOREGROUND, STATUS_BAR_BORDER } from 'vs/workbench/common/theme';
import { IDebugService } from 'vs/workbench/contrib/debug/common/debug';
import { localize } from 'vs/nls';
import { combinedDisposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { DomEmitter } from 'vs/base/browser/event';
import { IStatusbarService, StatusbarAlignment } from 'vs/workbench/services/statusbar/browser/statusbar';
export const STATUS_BAR_OFFLINE_BACKGROUND = registerColor('statusBar.offlineBackground', {
dark: '#6c1717',
light: '#6c1717',
hcDark: '#6c1717',
hcLight: '#6c1717'
}, localize('statusBarOfflineBackground', "Status bar background color when the workbench is offline. The status bar is shown in the bottom of the window"));
export const STATUS_BAR_OFFLINE_FOREGROUND = registerColor('statusBar.offlineForeground', {
dark: STATUS_BAR_FOREGROUND,
light: STATUS_BAR_FOREGROUND,
hcDark: STATUS_BAR_FOREGROUND,
hcLight: STATUS_BAR_FOREGROUND
}, localize('statusBarOfflineForeground', "Status bar foreground color when the workbench is offline. The status bar is shown in the bottom of the window"));
export const STATUS_BAR_OFFLINE_BORDER = registerColor('statusBar.offlineBorder', {
dark: STATUS_BAR_BORDER,
light: STATUS_BAR_BORDER,
hcDark: STATUS_BAR_BORDER,
hcLight: STATUS_BAR_BORDER
}, localize('statusBarOfflineBorder', "Status bar border color separating to the sidebar and editor when the workbench is offline. The status bar is shown in the bottom of the window"));
export class OfflineStatusBarController implements IWorkbenchContribution {
private readonly disposables = new DisposableStore();
private disposable: IDisposable | undefined;
private set enabled(enabled: boolean) {
if (enabled === !!this.disposable) {
return;
}
if (enabled) {
this.disposable = combinedDisposable(
this.statusbarService.overrideStyle({
priority: 100,
foreground: STATUS_BAR_OFFLINE_FOREGROUND,
background: STATUS_BAR_OFFLINE_BACKGROUND,
border: STATUS_BAR_OFFLINE_BORDER,
}),
this.statusbarService.addEntry({
name: 'Offline Indicator',
text: '$(debug-disconnect) Offline',
ariaLabel: 'Network is offline.',
tooltip: localize('offline', "Network appears to be offline, certain features might be unavailable.")
}, 'offline', StatusbarAlignment.LEFT, 10000)
);
} else {
this.disposable!.dispose();
this.disposable = undefined;
}
}
constructor(
@IDebugService private readonly debugService: IDebugService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IStatusbarService private readonly statusbarService: IStatusbarService
) {
Event.any(
this.disposables.add(new DomEmitter(window, 'online')).event,
this.disposables.add(new DomEmitter(window, 'offline')).event
)(this.update, this, this.disposables);
this.debugService.onDidChangeState(this.update, this, this.disposables);
this.contextService.onDidChangeWorkbenchState(this.update, this, this.disposables);
this.update();
}
protected update(): void {
this.enabled = !navigator.onLine;
}
dispose(): void {
this.disposable?.dispose();
this.disposables.dispose();
}
}
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench)
.registerWorkbenchContribution(OfflineStatusBarController, LifecyclePhase.Restored);

View file

@ -6,7 +6,9 @@
import * as nls from 'vs/nls';
import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from 'vs/workbench/services/remote/common/remoteAgentService';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Event } from 'vs/base/common/event';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
@ -42,6 +44,22 @@ import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } f
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IProductService } from 'vs/platform/product/common/productService';
import { DomEmitter } from 'vs/base/browser/event';
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
export const STATUS_BAR_OFFLINE_BACKGROUND = registerColor('statusBar.offlineBackground', {
dark: '#6c1717',
light: '#6c1717',
hcDark: '#6c1717',
hcLight: '#6c1717'
}, nls.localize('statusBarOfflineBackground', "Status bar background color when the workbench is offline. The status bar is shown in the bottom of the window"));
export const STATUS_BAR_OFFLINE_FOREGROUND = registerColor('statusBar.offlineForeground', {
dark: STATUS_BAR_HOST_NAME_FOREGROUND,
light: STATUS_BAR_HOST_NAME_FOREGROUND,
hcDark: STATUS_BAR_HOST_NAME_FOREGROUND,
hcLight: STATUS_BAR_HOST_NAME_FOREGROUND
}, nls.localize('statusBarOfflineForeground', "Status bar foreground color when the workbench is offline. The status bar is shown in the bottom of the window"));
type ActionGroup = [string, Array<MenuItemAction | SubmenuItemAction>];
export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution {
@ -53,6 +71,9 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
private static readonly REMOTE_STATUS_LABEL_MAX_LENGTH = 40;
private static readonly REMOTE_CONNECTION_LATENCY_SCHEDULER_DELAY = 60 * 1000;
private static readonly REMOTE_CONNECTION_LATENCY_SCHEDULER_FIRST_RUN_DELAY = 10 * 1000;
private remoteStatusEntry: IStatusbarEntryAccessor | undefined;
private readonly legacyIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); // to be removed once migration completed
@ -67,6 +88,9 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
private connectionState: 'initializing' | 'connected' | 'reconnecting' | 'disconnected' | undefined = undefined;
private readonly connectionStateContextKey = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', '').bindTo(this.contextKeyService);
private networkState: 'online' | 'offline' | 'high-latency' | undefined = undefined;
private measureNetworkConnectionLatencyScheduler: RunOnceScheduler | undefined = undefined;
private loggedInvalidGroupNames: { [group: string]: boolean } = Object.create(null);
constructor(
@ -172,8 +196,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
};
});
}
}
private registerListeners(): void {
@ -205,13 +227,13 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
case PersistentConnectionEventType.ConnectionLost:
case PersistentConnectionEventType.ReconnectionRunning:
case PersistentConnectionEventType.ReconnectionWait:
this.setState('reconnecting');
this.setConnectionState('reconnecting');
break;
case PersistentConnectionEventType.ReconnectionPermanentFailure:
this.setState('disconnected');
this.setConnectionState('disconnected');
break;
case PersistentConnectionEventType.ConnectionGain:
this.setState('connected');
this.setConnectionState('connected');
break;
}
}));
@ -222,6 +244,14 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
this.updateRemoteStatusIndicator();
}));
}
// Online / Offline changes (web only)
if (isWeb) {
this._register(Event.any(
this._register(new DomEmitter(window, 'online')).event,
this._register(new DomEmitter(window, 'offline')).event
)(() => this.setNetworkState(navigator.onLine ? 'online' : 'offline')));
}
}
private updateVirtualWorkspaceLocation() {
@ -239,9 +269,9 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
try {
await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority);
this.setState('connected');
this.setConnectionState('connected');
} catch (error) {
this.setState('disconnected');
this.setConnectionState('disconnected');
}
})();
}
@ -249,7 +279,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
this.updateRemoteStatusIndicator();
}
private setState(newState: 'disconnected' | 'connected' | 'reconnecting'): void {
private setConnectionState(newState: 'disconnected' | 'connected' | 'reconnecting'): void {
if (this.connectionState !== newState) {
this.connectionState = newState;
@ -260,6 +290,53 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
this.connectionStateContextKey.set(this.connectionState);
}
// indicate status
this.updateRemoteStatusIndicator();
// start measuring connection latency once connected
if (newState === 'connected') {
this.scheduleMeasureNetworkConnectionLatency();
}
}
}
private scheduleMeasureNetworkConnectionLatency(): void {
if (
!this.remoteAuthority || // only when having a remote connection
this.measureNetworkConnectionLatencyScheduler // already scheduled
) {
return;
}
this.measureNetworkConnectionLatencyScheduler = this._register(new RunOnceScheduler(() => this.measureNetworkConnectionLatency(), RemoteStatusIndicator.REMOTE_CONNECTION_LATENCY_SCHEDULER_DELAY));
this.measureNetworkConnectionLatencyScheduler.schedule(RemoteStatusIndicator.REMOTE_CONNECTION_LATENCY_SCHEDULER_FIRST_RUN_DELAY);
}
private async measureNetworkConnectionLatency(): Promise<void> {
// Measure latency if we are online
// but only when the window has focus to prevent constantly
// waking up the connection to the remote
if (this.hostService.hasFocus && this.networkState !== 'offline') {
const measurement = await remoteConnectionLatencyMeasurer.measure(this.remoteAgentService);
if (measurement) {
if (measurement.high) {
this.setNetworkState('high-latency');
} else if (this.networkState === 'high-latency') {
this.setNetworkState('online');
}
}
}
this.measureNetworkConnectionLatencyScheduler?.schedule();
}
private setNetworkState(newState: 'online' | 'offline' | 'high-latency'): void {
if (this.networkState !== newState) {
this.networkState = newState;
// update status
this.updateRemoteStatusIndicator();
}
}
@ -287,7 +364,12 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
// Remote Indicator: show if provided via options, e.g. by the web embedder API
const remoteIndicator = this.environmentService.options?.windowIndicator;
if (remoteIndicator) {
this.renderRemoteStatusIndicator(truncate(remoteIndicator.label, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH), remoteIndicator.tooltip, remoteIndicator.command);
let remoteIndicatorLabel = remoteIndicator.label.trim();
if (!remoteIndicatorLabel.startsWith('$(')) {
remoteIndicatorLabel = `$(remote) ${remoteIndicatorLabel}`; // ensure the indicator has a codicon
}
this.renderRemoteStatusIndicator(truncate(remoteIndicatorLabel, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH), remoteIndicator.tooltip, remoteIndicator.command);
return;
}
@ -299,7 +381,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
this.renderRemoteStatusIndicator(nls.localize('host.open', "Opening Remote..."), nls.localize('host.open', "Opening Remote..."), undefined, true /* progress */);
break;
case 'reconnecting':
this.renderRemoteStatusIndicator(`${nls.localize('host.reconnecting', "Reconnecting to {0}...", truncate(hostLabel, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH))}`, undefined, undefined, true);
this.renderRemoteStatusIndicator(`${nls.localize('host.reconnecting', "Reconnecting to {0}...", truncate(hostLabel, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH))}`, undefined, undefined, true /* progress */);
break;
case 'disconnected':
this.renderRemoteStatusIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from {0}", truncate(hostLabel, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH))}`);
@ -319,6 +401,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
}
// Show when in a virtual workspace
if (this.virtualWorkspaceLocation) {
// Workspace with label: indicate editing source
const workspaceLabel = this.labelService.getHostLabel(this.virtualWorkspaceLocation.scheme, this.virtualWorkspaceLocation.authority);
if (workspaceLabel) {
@ -341,6 +424,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
return;
}
}
// Show when there are commands other than the 'install additional remote extensions' command.
if (this.hasRemoteMenuCommands(true)) {
this.renderRemoteStatusIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a Remote Window"));
@ -352,22 +436,18 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
this.remoteStatusEntry = undefined;
}
private renderRemoteStatusIndicator(text: string, tooltip?: string | IMarkdownString, command?: string, showProgress?: boolean): void {
const name = nls.localize('remoteHost', "Remote Host");
if (typeof command !== 'string' && (this.hasRemoteMenuCommands(false))) {
command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID;
}
private renderRemoteStatusIndicator(initialText: string, initialTooltip?: string | MarkdownString, command?: string, showProgress?: boolean): void {
const { text, tooltip, ariaLabel } = this.withNetworkStatus(initialText, initialTooltip);
const ariaLabel = getCodiconAriaLabel(text);
const properties: IStatusbarEntry = {
name,
backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND),
color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND),
name: nls.localize('remoteHost', "Remote Host"),
backgroundColor: themeColorFromId(this.networkState === 'offline' ? STATUS_BAR_OFFLINE_BACKGROUND : STATUS_BAR_HOST_NAME_BACKGROUND),
color: themeColorFromId(this.networkState === 'offline' ? STATUS_BAR_OFFLINE_FOREGROUND : STATUS_BAR_HOST_NAME_FOREGROUND),
ariaLabel,
text,
showProgress,
tooltip,
command
command: command ?? this.hasRemoteMenuCommands(false) ? RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID : undefined
};
if (this.remoteStatusEntry) {
@ -377,6 +457,59 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
}
}
private withNetworkStatus(initialText: string, initialTooltip?: string | MarkdownString): { text: string; tooltip: string | IMarkdownString | undefined; ariaLabel: string } {
let text = initialText;
let tooltip = initialTooltip;
let ariaLabel = getCodiconAriaLabel(text);
// `initialText` can have a "$(remote)" codicon in the beginning
// but it may not have it depending on the environment.
// the following function will replace the codicon in the beginning with
// another icon or add it to the beginning if no icon
function insertOrReplaceCodicon(target: string, codicon: string): string {
if (target.startsWith('$(remote)')) {
return target.replace('$(remote)', codicon);
}
return `${codicon} ${target}`;
}
switch (this.networkState) {
case 'offline': {
text = insertOrReplaceCodicon(initialText, '$(alert)');
const offlineMessage = nls.localize('networkStatusOfflineTooltip', "Network appears to be offline, certain features might be unavailable.");
tooltip = this.appendTooltipLine(tooltip, offlineMessage);
ariaLabel = `${ariaLabel}, ${offlineMessage}`;
break;
}
case 'high-latency':
text = insertOrReplaceCodicon(initialText, '$(alert)');
tooltip = this.appendTooltipLine(tooltip, nls.localize('networkStatusHighLatencyTooltip', "Network appears to have high latency ({0}ms last, {1}ms average), certain features may be slow to respond.", remoteConnectionLatencyMeasurer.latency?.current?.toFixed(2), remoteConnectionLatencyMeasurer.latency?.average?.toFixed(2)));
break;
}
return { text, tooltip, ariaLabel };
}
private appendTooltipLine(tooltip: string | MarkdownString | undefined, line: string): MarkdownString {
let markdownTooltip: MarkdownString;
if (typeof tooltip === 'string') {
markdownTooltip = new MarkdownString(tooltip, { isTrusted: true, supportThemeIcons: true });
} else {
markdownTooltip = tooltip ?? new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
}
if (markdownTooltip.value.length > 0) {
markdownTooltip.appendText('\n\n');
}
markdownTooltip.appendText(line);
return markdownTooltip;
}
private showRemoteMenu() {
const getCategoryLabel = (action: MenuItemAction) => {
if (action.item.category) {

View file

@ -9,7 +9,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { ILabelService, ResourceLabelFormatting } from 'vs/platform/label/common/label';
import { OperatingSystem, isWeb, OS } from 'vs/base/common/platform';
import { Schemas } from 'vs/base/common/network';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from 'vs/workbench/services/remote/common/remoteAgentService';
import { ILoggerService } from 'vs/platform/log/common/log';
import { localize } from 'vs/nls';
import { Disposable } from 'vs/base/common/lifecycle';
@ -28,7 +28,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
import { IDownloadService } from 'vs/platform/download/common/download';
import { DownloadServiceChannel } from 'vs/platform/download/common/downloadIpc';
import { timeout } from 'vs/base/common/async';
import { RemoteLoggerChannelClient } from 'vs/platform/log/common/logIpc';
export class LabelContribution implements IWorkbenchContribution {
@ -140,9 +139,6 @@ class RemoteInvalidWorkspaceDetector extends Disposable implements IWorkbenchCon
}
}
const EXT_HOST_LATENCY_SAMPLES = 5;
const EXT_HOST_LATENCY_DELAY = 2_000;
class InitialRemoteConnectionHealthContribution implements IWorkbenchContribution {
constructor(
@ -206,15 +202,9 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio
}
private async _measureExtHostLatency() {
// Get the minimum latency, since latency spikes could be caused by a busy extension host.
let bestLatency = Infinity;
for (let i = 0; i < EXT_HOST_LATENCY_SAMPLES; i++) {
const rtt = await this._remoteAgentService.getRoundTripTime();
if (rtt === undefined) {
return;
}
bestLatency = Math.min(bestLatency, rtt / 2);
await timeout(EXT_HOST_LATENCY_DELAY);
const measurement = await remoteConnectionLatencyMeasurer.measure(this._remoteAgentService);
if (measurement === undefined) {
return;
}
type RemoteConnectionLatencyClassification = {
@ -233,7 +223,7 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio
this._telemetryService.publicLog2<RemoteConnectionLatencyEvent, RemoteConnectionLatencyClassification>('remoteConnectionLatency', {
web: isWeb,
remoteName: getRemoteName(this._environmentService.remoteAuthority),
latencyMs: bestLatency
latencyMs: measurement.current
});
}
}

View file

@ -5,7 +5,7 @@
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IRemoteAgentService, remoteConnectionLatencyMeasurer } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Disposable } from 'vs/base/common/lifecycle';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
@ -44,6 +44,12 @@ class RemoteAgentDiagnosticListener implements IWorkbenchContribution {
.then(info => {
if (info) {
(info as IRemoteDiagnosticInfo).hostName = hostName;
if (remoteConnectionLatencyMeasurer.latency?.high) {
(info as IRemoteDiagnosticInfo).latency = {
average: remoteConnectionLatencyMeasurer.latency.average,
current: remoteConnectionLatencyMeasurer.latency.current
};
}
}
ipcRenderer.send(request.replyChannel, info);

View file

@ -10,6 +10,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics
import { Event } from 'vs/base/common/event';
import { PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection';
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
import { timeout } from 'vs/base/common/async';
export const IRemoteAgentService = createDecorator<IRemoteAgentService>('remoteAgentService');
@ -59,3 +60,94 @@ export interface IRemoteAgentConnection {
registerChannel<T extends IServerChannel<RemoteAgentConnectionContext>>(channelName: string, channel: T): void;
getInitialConnectionTimeMs(): Promise<number>;
}
export interface IRemoteConnectionLatencyMeasurement {
readonly initial: number | undefined;
readonly current: number;
readonly average: number;
readonly high: boolean;
}
export const remoteConnectionLatencyMeasurer = new class {
readonly #maxSampleCount = 5;
readonly #sampleDelay = 2000;
readonly #initial: number[] = [];
readonly #maxInitialCount = 3;
readonly #average: number[] = [];
readonly #maxAverageCount = 100;
readonly #highLatencyMultiple = 2;
readonly #highLatencyMinThreshold = 500;
readonly #highLatencyMaxThreshold = 1500;
#lastMeasurement: IRemoteConnectionLatencyMeasurement | undefined = undefined;
get latency() { return this.#lastMeasurement; }
async measure(remoteAgentService: IRemoteAgentService): Promise<IRemoteConnectionLatencyMeasurement | undefined> {
let currentLatency = Infinity;
// Measure up to samples count
for (let i = 0; i < this.#maxSampleCount; i++) {
const rtt = await remoteAgentService.getRoundTripTime();
if (rtt === undefined) {
return undefined;
}
currentLatency = Math.min(currentLatency, rtt / 2 /* we want just one way, not round trip time */);
await timeout(this.#sampleDelay);
}
// Keep track of average latency
this.#average.push(currentLatency);
if (this.#average.length > this.#maxAverageCount) {
this.#average.shift();
}
// Keep track of initial latency
let initialLatency: number | undefined = undefined;
if (this.#initial.length < this.#maxInitialCount) {
this.#initial.push(currentLatency);
} else {
initialLatency = this.#initial.reduce((sum, value) => sum + value, 0) / this.#initial.length;
}
// Remember as last measurement
this.#lastMeasurement = {
initial: initialLatency,
current: currentLatency,
average: this.#average.reduce((sum, value) => sum + value, 0) / this.#average.length,
high: (() => {
// based on the initial, average and current latency, try to decide
// if the connection has high latency
// Some rules:
// - we require the initial latency to be computed
// - we only consider latency above highLatencyMinThreshold as potentially high
// - we require the current latency to be above the average latency by a factor of highLatencyMultiple
// - but not if the latency is actually above highLatencyMaxThreshold
if (typeof initialLatency === 'undefined') {
return false;
}
if (currentLatency > this.#highLatencyMaxThreshold) {
return true;
}
if (currentLatency > this.#highLatencyMinThreshold && currentLatency > initialLatency * this.#highLatencyMultiple) {
return true;
}
return false;
})()
};
return this.#lastMeasurement;
}
};

View file

@ -157,9 +157,6 @@ import 'vs/workbench/contrib/issue/browser/issue.contribution';
// Splash
import 'vs/workbench/contrib/splash/browser/splash.contribution';
// Offline
import 'vs/workbench/contrib/offline/browser/offline.contribution';
//#endregion