Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exploration: Better transferring of TypedArrays used in Webview.postMessage #115664

Merged
merged 4 commits into from Mar 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion extensions/vscode-api-tests/package.json
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
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
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
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
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
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
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
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