Skip to content

Commit

Permalink
Transfer webview resource buffers (#139145)
Browse files Browse the repository at this point in the history
* Use transferables for webview resources

Fixes #139145

This updates the webview resource loading to use transferables

On desktop, this requires a new way of converting the file stream to a buffer without using the nodejs backing pool

* Use ArrayBuffer directly instead of using Buffer
  • Loading branch information
mjbvz committed Feb 3, 2022
1 parent 9b75370 commit 256cc1d
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 15 deletions.
8 changes: 4 additions & 4 deletions src/vs/base/common/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export function isReadableBufferedStream<T>(obj: unknown): obj is ReadableBuffer
return isReadableStream(candidate.stream) && Array.isArray(candidate.buffer) && typeof candidate.ended === 'boolean';
}

export interface IReducer<T> {
(data: T[]): T;
export interface IReducer<T, R = T> {
(data: T[]): R;
}

export interface IDataTransformer<Original, Transformed> {
Expand Down Expand Up @@ -504,9 +504,9 @@ export function peekReadable<T>(readable: Readable<T>, reducer: IReducer<T>, max
* a stream fully, awaiting all the events without caring
* about the data.
*/
export function consumeStream<T>(stream: ReadableStreamEvents<T>, reducer: IReducer<T>): Promise<T>;
export function consumeStream<T, R = T>(stream: ReadableStreamEvents<T>, reducer: IReducer<T, R>): Promise<R>;
export function consumeStream(stream: ReadableStreamEvents<unknown>): Promise<undefined>;
export function consumeStream<T>(stream: ReadableStreamEvents<T>, reducer?: IReducer<T>): Promise<T | undefined> {
export function consumeStream<T, R = T>(stream: ReadableStreamEvents<T>, reducer?: IReducer<T, R>): Promise<R | undefined> {
return new Promise((resolve, reject) => {
const chunks: T[] = [];

Expand Down
29 changes: 19 additions & 10 deletions src/vs/workbench/contrib/webview/browser/webviewElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { addDisposableListener } from 'vs/base/browser/dom';
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { IAction } from 'vs/base/common/actions';
import { ThrottledDelayer } from 'vs/base/common/async';
import { streamToBuffer } from 'vs/base/common/buffer';
import { streamToBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
Expand All @@ -27,8 +27,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ITunnelService } from 'vs/platform/tunnel/common/tunnel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITunnelService } from 'vs/platform/tunnel/common/tunnel';
import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping';
import { asWebviewUri, decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/common/webview';
import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/browser/resourceLoading';
Expand Down Expand Up @@ -83,7 +83,11 @@ namespace WebviewState {
readonly type = Type.Initializing;

constructor(
public readonly pendingMessages: Array<{ readonly channel: string; readonly data?: any }>
public readonly pendingMessages: Array<{
readonly channel: string;
readonly data?: any;
readonly transferable: Transferable[];
}>
) { }
}

Expand Down Expand Up @@ -403,11 +407,11 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
this._send('message', { message, transfer });
}

protected _send(channel: string, data?: any): void {
protected _send(channel: string, data?: any, transferable: Transferable[] = []): void {
if (this._state.type === WebviewState.Type.Initializing) {
this._state.pendingMessages.push({ channel, data });
this._state.pendingMessages.push({ channel, data, transferable });
} else {
this.doPostMessage(channel, data);
this.doPostMessage(channel, data, transferable);
}
}

Expand Down Expand Up @@ -480,9 +484,9 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
return this._webviewContentOrigin;
}

private doPostMessage(channel: string, data?: any): void {
private doPostMessage(channel: string, data?: any, transferable: Transferable[] = []): void {
if (this.element && this.messagePort) {
this.messagePort.postMessage({ channel, args: data });
this.messagePort.postMessage({ channel, args: data }, transferable);
}
}

Expand Down Expand Up @@ -707,7 +711,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD

switch (result.type) {
case WebviewResourceResponse.Type.Success: {
const { buffer } = await streamToBuffer(result.stream);
const buffer = await this.streamToBuffer(result.stream);
return this._send('did-load-resource', {
id,
status: 200,
Expand All @@ -716,7 +720,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
data: buffer,
etag: result.etag,
mtime: result.mtime
});
}, [buffer]);
}
case WebviewResourceResponse.Type.NotModified: {
return this._send('did-load-resource', {
Expand Down Expand Up @@ -746,6 +750,11 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
});
}

protected async streamToBuffer(stream: VSBufferReadableStream): Promise<ArrayBufferLike> {
const vsBuffer = await streamToBuffer(stream);
return vsBuffer.buffer;
}

private async localLocalhost(id: string, origin: string) {
const authority = this._environmentService.remoteAuthority;
const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
*--------------------------------------------------------------------------------------------*/

import { Delayer } from 'vs/base/common/async';
import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
import { Schemas } from 'vs/base/common/network';
import { consumeStream } from 'vs/base/common/stream';
import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
Expand All @@ -16,8 +18,8 @@ import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { ITunnelService } from 'vs/platform/tunnel/common/tunnel';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITunnelService } from 'vs/platform/tunnel/common/tunnel';
import { FindInFrameOptions, IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
import { WebviewContentOptions, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview';
Expand Down Expand Up @@ -94,6 +96,22 @@ export class ElectronWebviewElement extends WebviewElement {
return `${Schemas.vscodeWebview}://${this.iframeId}`;
}

protected override streamToBuffer(stream: VSBufferReadableStream): Promise<ArrayBufferLike> {
// Join buffers from stream without using the Node.js backing pool.
// This lets us transfer the resulting buffer to the webview.
return consumeStream<VSBuffer, ArrayBufferLike>(stream, (buffers: readonly VSBuffer[]) => {
const totalLength = buffers.reduce((prev, curr) => prev + curr.byteLength, 0);
const ret = new ArrayBuffer(totalLength);
const view = new Uint8Array(ret);
let offset = 0;
for (const element of buffers) {
view.set(element.buffer, offset);
offset += element.byteLength;
}
return ret;
});
}

/**
* Webviews expose a stateful find API.
* Successive calls to find will move forward or backward through onFindResults
Expand Down

1 comment on commit 256cc1d

@jeanp413
Copy link
Contributor

@jeanp413 jeanp413 commented on 256cc1d Feb 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjbvz There's a typo in browser/webviewElement::streamToBuffer, should be vsBuffer.buffer.buffer; as it is now loading web view resources in vscode server is broken. Created a PR with the fix #142288

Please sign in to comment.