Skip to content

Commit

Permalink
Exploration: Better transferring of TypedArrays used in Webview.postM…
Browse files Browse the repository at this point in the history
…essage (#115664)

* Improve passing of ArrayBuffers to and from webviews

Fixes #115807

* Serialize and restore typed arrays too

This also makes it so that if you pass the same ArrayBuffer twice in an object, we use a single object on the receiver side too

* Fix spelling

* Require VS Code 1.56+
  • Loading branch information
mjbvz committed Mar 30, 2021
1 parent 242bea8 commit 3499f63
Show file tree
Hide file tree
Showing 20 changed files with 434 additions and 67 deletions.
2 changes: 1 addition & 1 deletion extensions/vscode-api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"activationEvents": [],
"main": "./out/extension",
"engines": {
"vscode": "^1.25.0"
"vscode": "^1.55.0"
},
"contributes": {
"configuration": {
Expand Down
112 changes: 112 additions & 0 deletions extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,118 @@ suite.skip('vscode API - webview', () => {
assert.strictEqual(await vscode.env.clipboard.readText(), expectedText);
});
}

test('webviews should transfer ArrayBuffers to and from webviews', async () => {
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
const ready = getMessage(webview);
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
<script>
const vscode = acquireVsCodeApi();
window.addEventListener('message', (message) => {
switch (message.data.type) {
case 'add1':
const arrayBuffer = message.data.array;
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < uint8Array.length; ++i) {
uint8Array[i] = uint8Array[i] + 1;
}
vscode.postMessage({ array: arrayBuffer }, [arrayBuffer]);
break;
}
});
vscode.postMessage({ type: 'ready' });
</script>`);
await ready;

const responsePromise = getMessage(webview);

const bufferLen = 100;

{
const arrayBuffer = new ArrayBuffer(bufferLen);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < bufferLen; ++i) {
uint8Array[i] = i;
}
webview.webview.postMessage({
type: 'add1',
array: arrayBuffer
});
}
{
const response = await responsePromise;
assert.ok(response.array instanceof ArrayBuffer);

const uint8Array = new Uint8Array(response.array);
for (let i = 0; i < bufferLen; ++i) {
assert.strictEqual(uint8Array[i], i + 1);
}
}
});

test('webviews should transfer Typed arrays to and from webviews', async () => {
const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { enableScripts: true, retainContextWhenHidden: true }));
const ready = getMessage(webview);
webview.webview.html = createHtmlDocumentWithBody(/*html*/`
<script>
const vscode = acquireVsCodeApi();
window.addEventListener('message', (message) => {
switch (message.data.type) {
case 'add1':
const uint8Array = message.data.array1;
// This should update both buffers since they use the same ArrayBuffer storage
const uint16Array = message.data.array2;
for (let i = 0; i < uint16Array.length; ++i) {
uint16Array[i] = uint16Array[i] + 1;
}
vscode.postMessage({ array1: uint8Array, array2: uint16Array, }, [uint16Array.buffer]);
break;
}
});
vscode.postMessage({ type: 'ready' });
</script>`);
await ready;

const responsePromise = getMessage(webview);

const bufferLen = 100;
{
const arrayBuffer = new ArrayBuffer(bufferLen);
const uint8Array = new Uint8Array(arrayBuffer);
const uint16Array = new Uint16Array(arrayBuffer);
for (let i = 0; i < uint16Array.length; ++i) {
uint16Array[i] = i;
}

webview.webview.postMessage({
type: 'add1',
array1: uint8Array,
array2: uint16Array,
});
}
{
const response = await responsePromise;

assert.ok(response.array1 instanceof Uint8Array);
assert.ok(response.array2 instanceof Uint16Array);
assert.ok(response.array1.buffer === response.array2.buffer);

const uint8Array = response.array1;
for (let i = 0; i < bufferLen; ++i) {
if (i % 2 === 0) {
assert.strictEqual(uint8Array[i], Math.floor(i / 2) + 1);
} else {
assert.strictEqual(uint8Array[i], 0);
}
}
}
});
});

function createHtmlDocumentWithBody(body: string): string {
Expand Down
22 changes: 22 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2850,6 +2850,28 @@ declare module 'vscode' {
*/
readonly triggerKind: CodeActionTriggerKind;
}

//#endregion

//#region https://github.com/microsoft/vscode/issues/115807

export interface Webview {
/**
* @param message A json serializable message to send to the webview.
*
* For older versions of vscode, if an `ArrayBuffer` is included in `message`,
* it will not be serialized properly and will not be received by the webview.
* Similarly any TypedArrays, such as a `Uint8Array`, will be very inefficiently
* serialized and will also not be recreated as a typed array inside the webview.
*
* However if your extension targets vscode 1.55+ in the `engines` field of its
* `package.json` any `ArrayBuffer` values that appear in `message` will be more
* efficiently transferred to the webview and will also be recreated inside of
* the webview.
*/
postMessage(message: any): Thenable<boolean>;
}

//#endregion

//#region https://github.com/microsoft/vscode/issues/115616 @alexr00
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/browser/mainThreadCodeInsets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape {
disposables.add(editor.onDidDispose(remove));
disposables.add(webviewZone);
disposables.add(webview);
disposables.add(webview.onMessage(msg => this._proxy.$onDidReceiveMessage(handle, msg)));
disposables.add(webview.onMessage(msg => this._proxy.$onDidReceiveMessage(handle, msg.message)));

this._insets.set(handle, webviewZone);
}
Expand Down
11 changes: 6 additions & 5 deletions src/vs/workbench/api/browser/mainThreadCustomEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,12 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
this._editorProviders.clear();
}

public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void {
this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true);
public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, serializeBuffersForPostMessage: boolean): void {
this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true, serializeBuffersForPostMessage);
}

public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void {
this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument);
public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: extHostProtocol.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean, serializeBuffersForPostMessage: boolean): void {
this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument, serializeBuffersForPostMessage);
}

private registerEditorProvider(
Expand All @@ -114,6 +114,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
options: extHostProtocol.IWebviewPanelOptions,
capabilities: extHostProtocol.CustomTextEditorCapabilities,
supportsMultipleEditorsPerDocument: boolean,
serializeBuffersForPostMessage: boolean,
): void {
if (this._editorProviders.has(viewType)) {
throw new Error(`Provider for ${viewType} already registered`);
Expand All @@ -133,7 +134,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
const handle = webviewInput.id;
const resource = webviewInput.resource;

this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput);
this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput, { serializeBuffersForPostMessage });
webviewInput.webview.options = options;
webviewInput.webview.extension = extension;

Expand Down
11 changes: 6 additions & 5 deletions src/vs/workbench/api/browser/mainThreadWebviewPanels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc

public get webviewInputs(): Iterable<WebviewInput> { return this._webviewInputs; }

public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput): void {
public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput, options: { serializeBuffersForPostMessage: boolean }): void {
this._webviewInputs.add(handle, input);
this._mainThreadWebviews.addWebview(handle, input.webview);
this._mainThreadWebviews.addWebview(handle, input.webview, options);

input.webview.onDidDispose(() => {
this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
Expand All @@ -156,6 +156,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
title: string;
webviewOptions: extHostProtocol.IWebviewOptions;
panelOptions: extHostProtocol.IWebviewPanelOptions;
serializeBuffersForPostMessage: boolean;
},
showOptions: { viewColumn?: EditorGroupColumn, preserveFocus?: boolean; },
): void {
Expand All @@ -168,7 +169,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
const extension = reviveWebviewExtension(extensionData);

const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), initData.title, mainThreadShowOptions, reviveWebviewOptions(initData.panelOptions), reviveWebviewContentOptions(initData.webviewOptions), extension);
this.addWebviewInput(handle, webview);
this.addWebviewInput(handle, webview, { serializeBuffersForPostMessage: initData.serializeBuffersForPostMessage });

/* __GDPR__
"webviews:createWebviewPanel" : {
Expand Down Expand Up @@ -205,7 +206,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
}
}

public $registerSerializer(viewType: string): void {
public $registerSerializer(viewType: string, options: { serializeBuffersForPostMessage: boolean }): void {
if (this._revivers.has(viewType)) {
throw new Error(`Reviver for ${viewType} already registered`);
}
Expand All @@ -223,7 +224,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc

const handle = webviewInput.id;

this.addWebviewInput(handle, webviewInput);
this.addWebviewInput(handle, webviewInput, options);

let state = undefined;
if (webviewInput.webview.state) {
Expand Down
4 changes: 2 additions & 2 deletions src/vs/workbench/api/browser/mainThreadWebviewViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc
public $registerWebviewViewProvider(
extensionData: extHostProtocol.WebviewExtensionDescription,
viewType: string,
options?: { retainContextWhenHidden?: boolean }
options: { retainContextWhenHidden?: boolean, serializeBuffersForPostMessage: boolean }
): void {
if (this._webviewViewProviders.has(viewType)) {
throw new Error(`View provider for ${viewType} already registered`);
Expand All @@ -68,7 +68,7 @@ export class MainThreadWebviewsViews extends Disposable implements extHostProtoc
const handle = webviewView.webview.id;

this._webviewViews.set(handle, webviewView);
this.mainThreadWebviews.addWebview(handle, webviewView.webview);
this.mainThreadWebviews.addWebview(handle, webviewView.webview, { serializeBuffersForPostMessage: options.serializeBuffersForPostMessage });

let state = undefined;
if (webviewView.webview.state) {
Expand Down
21 changes: 15 additions & 6 deletions src/vs/workbench/api/browser/mainThreadWebviews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { VSBuffer } from 'vs/base/common/buffer';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { isWeb } from 'vs/base/common/platform';
Expand All @@ -13,6 +14,8 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProductService } from 'vs/platform/product/common/productService';
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
import { serializeMessage } from 'vs/workbench/api/common/extHostWebview';
import { deserializeWebviewMessage } from 'vs/workbench/api/common/extHostWebviewMessaging';
import { Webview, WebviewContentOptions, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';

export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape {
Expand All @@ -39,13 +42,13 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews);
}

public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay): void {
public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay, options: { serializeBuffersForPostMessage: boolean }): void {
if (this._webviews.has(handle)) {
throw new Error('Webview already registered');
}

this._webviews.set(handle, webview);
this.hookupWebviewEventDelegate(handle, webview);
this.hookupWebviewEventDelegate(handle, webview, options);
}

public $setHtml(handle: extHostProtocol.WebviewHandle, value: string): void {
Expand All @@ -58,17 +61,23 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
webview.contentOptions = reviveWebviewContentOptions(options);
}

public async $postMessage(handle: extHostProtocol.WebviewHandle, message: any): Promise<boolean> {
public async $postMessage(handle: extHostProtocol.WebviewHandle, jsonMessage: string, ...buffers: VSBuffer[]): Promise<boolean> {
const webview = this.getWebview(handle);
webview.postMessage(message);
const { message, arrayBuffers } = deserializeWebviewMessage(jsonMessage, buffers);
webview.postMessage(message, arrayBuffers);
return true;
}

private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay) {
private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay, options: { serializeBuffersForPostMessage: boolean }) {
const disposables = new DisposableStore();

disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri)));
disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); }));

disposables.add(webview.onMessage((message) => {
const serialized = serializeMessage(message.message, options);
this._proxy.$onMessage(handle, serialized.message, ...serialized.buffers);
}));

disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value)));

disposables.add(webview.onDidDispose(() => {
Expand Down

0 comments on commit 3499f63

Please sign in to comment.