mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
parent
09f80f4c54
commit
487a08afe5
|
@ -322,10 +322,6 @@
|
||||||
"name": "vs/workbench/contrib/bracketPairColorizer2Telemetry",
|
"name": "vs/workbench/contrib/bracketPairColorizer2Telemetry",
|
||||||
"project": "vscode-workbench"
|
"project": "vscode-workbench"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "vs/workbench/contrib/offline",
|
|
||||||
"project": "vscode-workbench"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "vs/workbench/contrib/remoteTunnel",
|
"name": "vs/workbench/contrib/remoteTunnel",
|
||||||
"project": "vscode-workbench"
|
"project": "vscode-workbench"
|
||||||
|
|
|
@ -95,6 +95,11 @@
|
||||||
"title": "Pause Connection (Test Reconnect)",
|
"title": "Pause Connection (Test Reconnect)",
|
||||||
"category": "Remote-TestResolver",
|
"category": "Remote-TestResolver",
|
||||||
"command": "vscode-testresolver.toggleConnectionPause"
|
"command": "vscode-testresolver.toggleConnectionPause"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Slowdown Connection (Test Slow Down Indicator)",
|
||||||
|
"category": "Remote-TestResolver",
|
||||||
|
"command": "vscode-testresolver.toggleConnectionSlowdown"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
|
|
|
@ -22,11 +22,25 @@ const enum CharCode {
|
||||||
|
|
||||||
let outputChannel: vscode.OutputChannel;
|
let outputChannel: vscode.OutputChannel;
|
||||||
|
|
||||||
|
const SLOWED_DOWN_CONNECTION_DELAY = 800;
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
|
||||||
let connectionPaused = false;
|
let connectionPaused = false;
|
||||||
const connectionPausedEvent = new vscode.EventEmitter<boolean>();
|
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'] {
|
function getTunnelFeatures(): vscode.TunnelInformation['tunnelFeatures'] {
|
||||||
return {
|
return {
|
||||||
elevation: true,
|
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> {
|
function doResolve(authority: string, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<vscode.ResolverResult> {
|
||||||
if (connectionPaused) {
|
if (connectionPaused) {
|
||||||
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable('Not available right now');
|
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable('Not available right now');
|
||||||
|
@ -237,13 +267,15 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
connectionPausedEvent.event(_ => handleConnectionPause());
|
connectionPausedEvent.event(_ => handleConnectionPause());
|
||||||
handleConnectionPause();
|
handleConnectionPause();
|
||||||
|
|
||||||
proxySocket.on('data', (data) => {
|
proxySocket.on('data', async (data) => {
|
||||||
|
await maybeSlowdown();
|
||||||
remoteReady = remoteSocket.write(data);
|
remoteReady = remoteSocket.write(data);
|
||||||
if (!remoteReady) {
|
if (!remoteReady) {
|
||||||
proxySocket.pause();
|
proxySocket.pause();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
remoteSocket.on('data', (data) => {
|
remoteSocket.on('data', async (data) => {
|
||||||
|
await maybeSlowdown();
|
||||||
localReady = proxySocket.write(data);
|
localReady = proxySocket.write(data);
|
||||||
if (!localReady) {
|
if (!localReady) {
|
||||||
remoteSocket.pause();
|
remoteSocket.pause();
|
||||||
|
@ -358,6 +390,22 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
connectionPausedEvent.fire(connectionPaused);
|
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 () => {
|
context.subscriptions.push(vscode.commands.registerCommand('vscode-testresolver.openTunnel', async () => {
|
||||||
const result = await vscode.window.showInputBox({
|
const result = await vscode.window.showInputBox({
|
||||||
prompt: 'Enter the remote port for the tunnel',
|
prompt: 'Enter the remote port for the tunnel',
|
||||||
|
|
|
@ -500,11 +500,10 @@ function doCreateUri(path: string, queryValues: Map<string, string>): URI {
|
||||||
// Create workbench
|
// Create workbench
|
||||||
create(document.body, {
|
create(document.body, {
|
||||||
...config,
|
...config,
|
||||||
settingsSyncOptions: config.settingsSyncOptions ? {
|
windowIndicator: config.windowIndicator ?? { label: '$(remote)', tooltip: `${product.nameShort} Web` },
|
||||||
enabled: config.settingsSyncOptions.enabled,
|
settingsSyncOptions: config.settingsSyncOptions ? { enabled: config.settingsSyncOptions.enabled, } : undefined,
|
||||||
} : undefined,
|
|
||||||
workspaceProvider: WorkspaceProvider.create(config),
|
workspaceProvider: WorkspaceProvider.create(config),
|
||||||
urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute),
|
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()
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -991,7 +991,7 @@ export class IssueReporter extends Disposable {
|
||||||
const remoteDataTable = $('table', undefined,
|
const remoteDataTable = $('table', undefined,
|
||||||
$('tr', undefined,
|
$('tr', undefined,
|
||||||
$('td', undefined, 'Remote'),
|
$('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,
|
$('tr', undefined,
|
||||||
$('td', undefined, 'OS'),
|
$('td', undefined, 'OS'),
|
||||||
|
|
|
@ -185,7 +185,7 @@ ${this.getInfos()}
|
||||||
|
|
||||||
|Item|Value|
|
|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}|
|
|OS|${remote.machineInfo.os}|
|
||||||
|CPUs|${remote.machineInfo.cpus}|
|
|CPUs|${remote.machineInfo.cpus}|
|
||||||
|Memory (System)|${remote.machineInfo.memory}|
|
|Memory (System)|${remote.machineInfo.memory}|
|
||||||
|
|
|
@ -52,6 +52,10 @@ export interface SystemInfo extends IMachineInfo {
|
||||||
|
|
||||||
export interface IRemoteDiagnosticInfo extends IDiagnosticInfo {
|
export interface IRemoteDiagnosticInfo extends IDiagnosticInfo {
|
||||||
hostName: string;
|
hostName: string;
|
||||||
|
latency?: {
|
||||||
|
current: number;
|
||||||
|
average: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRemoteDiagnosticError {
|
export interface IRemoteDiagnosticError {
|
||||||
|
|
|
@ -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);
|
|
|
@ -6,7 +6,9 @@
|
||||||
import * as nls from 'vs/nls';
|
import * as nls from 'vs/nls';
|
||||||
import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme';
|
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 { 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 { Disposable, dispose } from 'vs/base/common/lifecycle';
|
||||||
import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
import { MenuId, IMenuService, MenuItemAction, MenuRegistry, registerAction2, Action2, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||||
|
@ -42,6 +44,22 @@ import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } f
|
||||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||||
import { IProductService } from 'vs/platform/product/common/productService';
|
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>];
|
type ActionGroup = [string, Array<MenuItemAction | SubmenuItemAction>];
|
||||||
export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution {
|
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_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 remoteStatusEntry: IStatusbarEntryAccessor | undefined;
|
||||||
|
|
||||||
private readonly legacyIndicatorMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); // to be removed once migration completed
|
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 connectionState: 'initializing' | 'connected' | 'reconnecting' | 'disconnected' | undefined = undefined;
|
||||||
private readonly connectionStateContextKey = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', '').bindTo(this.contextKeyService);
|
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);
|
private loggedInvalidGroupNames: { [group: string]: boolean } = Object.create(null);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -172,8 +196,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerListeners(): void {
|
private registerListeners(): void {
|
||||||
|
@ -205,13 +227,13 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
|
||||||
case PersistentConnectionEventType.ConnectionLost:
|
case PersistentConnectionEventType.ConnectionLost:
|
||||||
case PersistentConnectionEventType.ReconnectionRunning:
|
case PersistentConnectionEventType.ReconnectionRunning:
|
||||||
case PersistentConnectionEventType.ReconnectionWait:
|
case PersistentConnectionEventType.ReconnectionWait:
|
||||||
this.setState('reconnecting');
|
this.setConnectionState('reconnecting');
|
||||||
break;
|
break;
|
||||||
case PersistentConnectionEventType.ReconnectionPermanentFailure:
|
case PersistentConnectionEventType.ReconnectionPermanentFailure:
|
||||||
this.setState('disconnected');
|
this.setConnectionState('disconnected');
|
||||||
break;
|
break;
|
||||||
case PersistentConnectionEventType.ConnectionGain:
|
case PersistentConnectionEventType.ConnectionGain:
|
||||||
this.setState('connected');
|
this.setConnectionState('connected');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@ -222,6 +244,14 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
|
||||||
this.updateRemoteStatusIndicator();
|
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() {
|
private updateVirtualWorkspaceLocation() {
|
||||||
|
@ -239,9 +269,9 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
|
||||||
try {
|
try {
|
||||||
await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority);
|
await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority);
|
||||||
|
|
||||||
this.setState('connected');
|
this.setConnectionState('connected');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState('disconnected');
|
this.setConnectionState('disconnected');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
@ -249,7 +279,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
|
||||||
this.updateRemoteStatusIndicator();
|
this.updateRemoteStatusIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setState(newState: 'disconnected' | 'connected' | 'reconnecting'): void {
|
private setConnectionState(newState: 'disconnected' | 'connected' | 'reconnecting'): void {
|
||||||
if (this.connectionState !== newState) {
|
if (this.connectionState !== newState) {
|
||||||
this.connectionState = newState;
|
this.connectionState = newState;
|
||||||
|
|
||||||
|
@ -260,6 +290,53 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
|
||||||
this.connectionStateContextKey.set(this.connectionState);
|
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();
|
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
|
// Remote Indicator: show if provided via options, e.g. by the web embedder API
|
||||||
const remoteIndicator = this.environmentService.options?.windowIndicator;
|
const remoteIndicator = this.environmentService.options?.windowIndicator;
|
||||||
if (remoteIndicator) {
|
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;
|
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 */);
|
this.renderRemoteStatusIndicator(nls.localize('host.open', "Opening Remote..."), nls.localize('host.open', "Opening Remote..."), undefined, true /* progress */);
|
||||||
break;
|
break;
|
||||||
case 'reconnecting':
|
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;
|
break;
|
||||||
case 'disconnected':
|
case 'disconnected':
|
||||||
this.renderRemoteStatusIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from {0}", truncate(hostLabel, RemoteStatusIndicator.REMOTE_STATUS_LABEL_MAX_LENGTH))}`);
|
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
|
// Show when in a virtual workspace
|
||||||
if (this.virtualWorkspaceLocation) {
|
if (this.virtualWorkspaceLocation) {
|
||||||
|
|
||||||
// Workspace with label: indicate editing source
|
// Workspace with label: indicate editing source
|
||||||
const workspaceLabel = this.labelService.getHostLabel(this.virtualWorkspaceLocation.scheme, this.virtualWorkspaceLocation.authority);
|
const workspaceLabel = this.labelService.getHostLabel(this.virtualWorkspaceLocation.scheme, this.virtualWorkspaceLocation.authority);
|
||||||
if (workspaceLabel) {
|
if (workspaceLabel) {
|
||||||
|
@ -341,6 +424,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show when there are commands other than the 'install additional remote extensions' command.
|
// Show when there are commands other than the 'install additional remote extensions' command.
|
||||||
if (this.hasRemoteMenuCommands(true)) {
|
if (this.hasRemoteMenuCommands(true)) {
|
||||||
this.renderRemoteStatusIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a Remote Window"));
|
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;
|
this.remoteStatusEntry = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderRemoteStatusIndicator(text: string, tooltip?: string | IMarkdownString, command?: string, showProgress?: boolean): void {
|
private renderRemoteStatusIndicator(initialText: string, initialTooltip?: string | MarkdownString, command?: string, showProgress?: boolean): void {
|
||||||
const name = nls.localize('remoteHost', "Remote Host");
|
const { text, tooltip, ariaLabel } = this.withNetworkStatus(initialText, initialTooltip);
|
||||||
if (typeof command !== 'string' && (this.hasRemoteMenuCommands(false))) {
|
|
||||||
command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ariaLabel = getCodiconAriaLabel(text);
|
|
||||||
const properties: IStatusbarEntry = {
|
const properties: IStatusbarEntry = {
|
||||||
name,
|
name: nls.localize('remoteHost', "Remote Host"),
|
||||||
backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND),
|
backgroundColor: themeColorFromId(this.networkState === 'offline' ? STATUS_BAR_OFFLINE_BACKGROUND : STATUS_BAR_HOST_NAME_BACKGROUND),
|
||||||
color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND),
|
color: themeColorFromId(this.networkState === 'offline' ? STATUS_BAR_OFFLINE_FOREGROUND : STATUS_BAR_HOST_NAME_FOREGROUND),
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
text,
|
text,
|
||||||
showProgress,
|
showProgress,
|
||||||
tooltip,
|
tooltip,
|
||||||
command
|
command: command ?? this.hasRemoteMenuCommands(false) ? RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.remoteStatusEntry) {
|
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() {
|
private showRemoteMenu() {
|
||||||
const getCategoryLabel = (action: MenuItemAction) => {
|
const getCategoryLabel = (action: MenuItemAction) => {
|
||||||
if (action.item.category) {
|
if (action.item.category) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
|
||||||
import { ILabelService, ResourceLabelFormatting } from 'vs/platform/label/common/label';
|
import { ILabelService, ResourceLabelFormatting } from 'vs/platform/label/common/label';
|
||||||
import { OperatingSystem, isWeb, OS } from 'vs/base/common/platform';
|
import { OperatingSystem, isWeb, OS } from 'vs/base/common/platform';
|
||||||
import { Schemas } from 'vs/base/common/network';
|
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 { ILoggerService } from 'vs/platform/log/common/log';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { Disposable } from 'vs/base/common/lifecycle';
|
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 { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
|
||||||
import { IDownloadService } from 'vs/platform/download/common/download';
|
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||||
import { DownloadServiceChannel } from 'vs/platform/download/common/downloadIpc';
|
import { DownloadServiceChannel } from 'vs/platform/download/common/downloadIpc';
|
||||||
import { timeout } from 'vs/base/common/async';
|
|
||||||
import { RemoteLoggerChannelClient } from 'vs/platform/log/common/logIpc';
|
import { RemoteLoggerChannelClient } from 'vs/platform/log/common/logIpc';
|
||||||
|
|
||||||
export class LabelContribution implements IWorkbenchContribution {
|
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 {
|
class InitialRemoteConnectionHealthContribution implements IWorkbenchContribution {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -206,15 +202,9 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _measureExtHostLatency() {
|
private async _measureExtHostLatency() {
|
||||||
// Get the minimum latency, since latency spikes could be caused by a busy extension host.
|
const measurement = await remoteConnectionLatencyMeasurer.measure(this._remoteAgentService);
|
||||||
let bestLatency = Infinity;
|
if (measurement === undefined) {
|
||||||
for (let i = 0; i < EXT_HOST_LATENCY_SAMPLES; i++) {
|
return;
|
||||||
const rtt = await this._remoteAgentService.getRoundTripTime();
|
|
||||||
if (rtt === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bestLatency = Math.min(bestLatency, rtt / 2);
|
|
||||||
await timeout(EXT_HOST_LATENCY_DELAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RemoteConnectionLatencyClassification = {
|
type RemoteConnectionLatencyClassification = {
|
||||||
|
@ -233,7 +223,7 @@ class InitialRemoteConnectionHealthContribution implements IWorkbenchContributio
|
||||||
this._telemetryService.publicLog2<RemoteConnectionLatencyEvent, RemoteConnectionLatencyClassification>('remoteConnectionLatency', {
|
this._telemetryService.publicLog2<RemoteConnectionLatencyEvent, RemoteConnectionLatencyClassification>('remoteConnectionLatency', {
|
||||||
web: isWeb,
|
web: isWeb,
|
||||||
remoteName: getRemoteName(this._environmentService.remoteAuthority),
|
remoteName: getRemoteName(this._environmentService.remoteAuthority),
|
||||||
latencyMs: bestLatency
|
latencyMs: measurement.current
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import * as nls from 'vs/nls';
|
import * as nls from 'vs/nls';
|
||||||
import { Registry } from 'vs/platform/registry/common/platform';
|
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 { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import { isMacintosh, isWindows } from 'vs/base/common/platform';
|
import { isMacintosh, isWindows } from 'vs/base/common/platform';
|
||||||
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
|
import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes';
|
||||||
|
@ -44,6 +44,12 @@ class RemoteAgentDiagnosticListener implements IWorkbenchContribution {
|
||||||
.then(info => {
|
.then(info => {
|
||||||
if (info) {
|
if (info) {
|
||||||
(info as IRemoteDiagnosticInfo).hostName = hostName;
|
(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);
|
ipcRenderer.send(request.replyChannel, info);
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics
|
||||||
import { Event } from 'vs/base/common/event';
|
import { Event } from 'vs/base/common/event';
|
||||||
import { PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection';
|
import { PersistentConnectionEvent } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||||
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
|
import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry';
|
||||||
|
import { timeout } from 'vs/base/common/async';
|
||||||
|
|
||||||
export const IRemoteAgentService = createDecorator<IRemoteAgentService>('remoteAgentService');
|
export const IRemoteAgentService = createDecorator<IRemoteAgentService>('remoteAgentService');
|
||||||
|
|
||||||
|
@ -59,3 +60,94 @@ export interface IRemoteAgentConnection {
|
||||||
registerChannel<T extends IServerChannel<RemoteAgentConnectionContext>>(channelName: string, channel: T): void;
|
registerChannel<T extends IServerChannel<RemoteAgentConnectionContext>>(channelName: string, channel: T): void;
|
||||||
getInitialConnectionTimeMs(): Promise<number>;
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -157,9 +157,6 @@ import 'vs/workbench/contrib/issue/browser/issue.contribution';
|
||||||
// Splash
|
// Splash
|
||||||
import 'vs/workbench/contrib/splash/browser/splash.contribution';
|
import 'vs/workbench/contrib/splash/browser/splash.contribution';
|
||||||
|
|
||||||
// Offline
|
|
||||||
import 'vs/workbench/contrib/offline/browser/offline.contribution';
|
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue