diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 5fcf6bc7b04..9bd72240a46 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -739,6 +739,10 @@ export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; $activateByEvent(activationEvent: string): Promise; + + $test_latency(n: number): Promise; + $test_up(b: Buffer): Promise; + $test_down(size: number): Promise; } export interface FileSystemEvents { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 13a4218c684..2cf4f9efeed 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -646,6 +646,20 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { .then(_ => this._activateByEvent(activationEvent, false)) ); } + + public async $test_latency(n: number): Promise { + return n; + } + + public async $test_up(b: Buffer): Promise { + return b.length; + } + + public async $test_down(size: number): Promise { + let b = Buffer.alloc(size, Math.random() % 256); + return b; + } + } function loadCommonJSModule(logService: ILogService, modulePath: string, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts index b7f836c30e3..ba1ad93bebd 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProcessManager.ts @@ -19,6 +19,14 @@ import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIden import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/node/rpcProtocol'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import * as nls from 'vs/nls'; +import { Action } from 'vs/base/common/actions'; +import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUntitledResourceInput } from 'vs/workbench/common/editor'; +import { StopWatch } from 'vs/base/common/stopwatch'; // Enable to see detailed message communication between window and extension host const LOG_EXTENSION_HOST_COMMUNICATION = false; @@ -71,6 +79,9 @@ export class ExtensionHostProcessManager extends Disposable { ); this._extensionHostProcessProxy.then(() => { initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent)); + this._register(registerLatencyTestProvider({ + measure: () => this.measure() + })); }); } @@ -94,6 +105,57 @@ export class ExtensionHostProcessManager extends Disposable { super.dispose(); } + private async measure(): Promise { + const latency = await this._measureLatency(); + const down = await this._measureDown(); + const up = await this._measureUp(); + return { + remoteAuthority: this._remoteAuthority, + latency, + down, + up + }; + } + + private async _measureLatency(): Promise { + const COUNT = 10; + + const { value: proxy } = await this._extensionHostProcessProxy; + let sum = 0; + for (let i = 0; i < COUNT; i++) { + const sw = StopWatch.create(true); + await proxy.$test_latency(i); + sw.stop(); + sum += sw.elapsed(); + } + return (sum / COUNT); + } + + private static _convert(byteCount: number, elapsedMillis: number): number { + return (byteCount * 1000 * 8) / elapsedMillis; + } + + private async _measureUp(): Promise { + const SIZE = 10 * 1024 * 1024; // 10MB + + const { value: proxy } = await this._extensionHostProcessProxy; + let b = Buffer.alloc(SIZE, Math.random() % 256); + const sw = StopWatch.create(true); + await proxy.$test_up(b); + sw.stop(); + return ExtensionHostProcessManager._convert(SIZE, sw.elapsed()); + } + + private async _measureDown(): Promise { + const SIZE = 10 * 1024 * 1024; // 10MB + + const { value: proxy } = await this._extensionHostProcessProxy; + const sw = StopWatch.create(true); + await proxy.$test_down(SIZE); + sw.stop(); + return ExtensionHostProcessManager._convert(SIZE, sw.elapsed()); + } + public canProfileExtensionHost(): boolean { return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort()); } @@ -238,3 +300,68 @@ class RPCLogger implements IRPCProtocolLogger { this._log('Win \u2192 Ext', this._totalOutgoing, msgLength, req, initiator, str, data); } } + +interface ExtHostLatencyResult { + remoteAuthority: string; + up: number; + down: number; + latency: number; +} + +interface ExtHostLatencyProvider { + measure(): Promise; +} + +let providers: ExtHostLatencyProvider[] = []; +function registerLatencyTestProvider(provider: ExtHostLatencyProvider): IDisposable { + providers.push(provider); + return { + dispose: () => { + for (let i = 0; i < providers.length; i++) { + if (providers[i] === provider) { + providers.splice(i, 1); + return; + } + } + } + }; +} + +function getLatencyTestProviders(): ExtHostLatencyProvider[] { + return providers.slice(0); +} + +export class MeasureExtHostLatencyAction extends Action { + public static readonly ID = 'editor.action.measureExtHostLatency'; + public static readonly LABEL = nls.localize('measureExtHostLatency', "Developer: Measure Extension Host Latency"); + + constructor( + id: string, + label: string, + @IEditorService private readonly _editorService: IEditorService + ) { + super(id, label); + } + + public async run(): Promise { + const measurements = await Promise.all(getLatencyTestProviders().map(provider => provider.measure())); + this._editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } } as IUntitledResourceInput); + } + + private static _print(m: ExtHostLatencyResult): string { + return `${m.remoteAuthority ? `Authority: ${m.remoteAuthority}\n` : ``}Roundtrip latency: ${m.latency.toFixed(3)}ms\nUp: ${MeasureExtHostLatencyAction._printSpeed(m.up)}\nDown: ${MeasureExtHostLatencyAction._printSpeed(m.down)}\n`; + } + + private static _printSpeed(n: number): string { + if (n <= 1024) { + return `${n} bps`; + } + if (n < 1024 * 1024) { + return `${(n / 1024).toFixed(1)} kbps`; + } + return `${(n / 1024 / 1024).toFixed(1)} Mbps`; + } +} + +const registry = Registry.as(ActionExtensions.WorkbenchActions); +registry.registerWorkbenchAction(new SyncActionDescriptor(MeasureExtHostLatencyAction, MeasureExtHostLatencyAction.ID, MeasureExtHostLatencyAction.LABEL), 'Developer: Measure Extension Host Latency');