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",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/contrib/offline",
|
||||
"project": "vscode-workbench"
|
||||
},
|
||||
{
|
||||
"name": "vs/workbench/contrib/remoteTunnel",
|
||||
"project": "vscode-workbench"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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()
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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}|
|
||||
|
|
|
@ -52,6 +52,10 @@ export interface SystemInfo extends IMachineInfo {
|
|||
|
||||
export interface IRemoteDiagnosticInfo extends IDiagnosticInfo {
|
||||
hostName: string;
|
||||
latency?: {
|
||||
current: number;
|
||||
average: number;
|
||||
};
|
||||
}
|
||||
|
||||
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 { 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) {
|
||||
|
|
|
@ -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,16 +202,10 @@ 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) {
|
||||
const measurement = await remoteConnectionLatencyMeasurer.measure(this._remoteAgentService);
|
||||
if (measurement === undefined) {
|
||||
return;
|
||||
}
|
||||
bestLatency = Math.min(bestLatency, rtt / 2);
|
||||
await timeout(EXT_HOST_LATENCY_DELAY);
|
||||
}
|
||||
|
||||
type RemoteConnectionLatencyClassification = {
|
||||
owner: 'connor4312';
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue