Revert service worker usage of MessageChannel (#140351)

* Revert service worker usage of MessageChannel

Reverts 66b6adf035

While I'm not 100% about this, I think 66b6adf035 causes resourses to occasionally not load. I believe this can happen if the service worker is unitilized while the webview remains active. I can't reproduce this myself so it may be related to memory pressure or resource usage, however relying on the service worker not being reinitilized does seem like a potentially bad idea https://stackoverflow.com/questions/34775105/what-causes-the-global-context-of-a-service-worker-to-be-reset

Will investigate if there's another way to achive this since using MessagePort did clean up the code and slightly improve performance

* Bump webview commit versions
This commit is contained in:
Matt Bierner 2022-01-10 14:22:49 -08:00 committed by GitHub
parent 942f56efa1
commit 9f867c3ed8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 180 additions and 154 deletions

View file

@ -25,7 +25,7 @@
"licenseFileName": "LICENSE.txt",
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
"urlProtocol": "code-oss",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/69df0500a8963fc469161c038a14a39384d5a303/out/vs/workbench/contrib/webview/browser/pre/",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/d372f9187401bd145a0a6e15ba369e2d82d02005/out/vs/workbench/contrib/webview/browser/pre/",
"extensionAllowedProposedApi": [
"ms-vscode.vscode-js-profile-flame",
"ms-vscode.vscode-js-profile-table",

View file

@ -206,27 +206,37 @@ const workerReady = new Promise((resolve, reject) => {
const swPath = `service-worker.js?v=${expectedWorkerVersion}&vscode-resource-base-authority=${searchParams.get('vscode-resource-base-authority')}`;
/**
* @param {MessageEvent} event
*/
const swMessageHandler = async (event) => {
if (event.data.channel !== 'init') {
console.log('Unknown message received in webview from service worker');
return;
}
navigator.serviceWorker.removeEventListener('message', swMessageHandler);
// Forward the port back to VS Code
hostMessaging.onMessage('did-init-service-worker', () => resolve());
hostMessaging.postMessage('init-service-worker', {}, event.ports);
};
navigator.serviceWorker.addEventListener('message', swMessageHandler);
navigator.serviceWorker.register(swPath)
.then(() => navigator.serviceWorker.ready)
.then(() => {
const initServiceWorker = (/** @type {ServiceWorker} */ worker) => {
worker.postMessage({ channel: 'init' });
.then(async registration => {
/**
* @param {MessageEvent} event
*/
const versionHandler = async (event) => {
if (event.data.channel !== 'version') {
return;
}
navigator.serviceWorker.removeEventListener('message', versionHandler);
if (event.data.version === expectedWorkerVersion) {
return resolve();
} else {
console.log(`Found unexpected service worker version. Found: ${event.data.version}. Expected: ${expectedWorkerVersion}`);
console.log(`Attempting to reload service worker`);
// If we have the wrong version, try once (and only once) to unregister and re-register
// Note that `.update` doesn't seem to work desktop electron at the moment so we use
// `unregister` and `register` here.
return registration.unregister()
.then(() => navigator.serviceWorker.register(swPath))
.then(() => navigator.serviceWorker.ready)
.finally(() => { resolve(); });
}
};
navigator.serviceWorker.addEventListener('message', versionHandler);
const postVersionMessage = (/** @type {ServiceWorker} */ controller) => {
controller.postMessage({ channel: 'version' });
};
// At this point, either the service worker is ready and
@ -236,13 +246,13 @@ const workerReady = new Promise((resolve, reject) => {
const currentController = navigator.serviceWorker.controller;
if (currentController?.scriptURL.endsWith(swPath)) {
// service worker already loaded & ready to receive messages
initServiceWorker(currentController);
postVersionMessage(currentController);
} else {
// either there's no controlling service worker, or it's an old one:
// wait for it to change before posting the message
const onControllerChange = () => {
navigator.serviceWorker.removeEventListener('controllerchange', onControllerChange);
initServiceWorker(navigator.serviceWorker.controller);
postVersionMessage(navigator.serviceWorker.controller);
};
navigator.serviceWorker.addEventListener('controllerchange', onControllerChange);
}
@ -376,7 +386,26 @@ const initData = {
themeName: undefined,
};
hostMessaging.onMessage('did-load-resource', (_event, data) => {
navigator.serviceWorker.ready.then(registration => {
assertIsDefined(registration.active).postMessage({ channel: 'did-load-resource', data }, data.data?.buffer ? [data.data.buffer] : []);
});
});
hostMessaging.onMessage('did-load-localhost', (_event, data) => {
navigator.serviceWorker.ready.then(registration => {
assertIsDefined(registration.active).postMessage({ channel: 'did-load-localhost', data });
});
});
navigator.serviceWorker.addEventListener('message', event => {
switch (event.data.channel) {
case 'load-resource':
case 'load-localhost':
hostMessaging.postMessage(event.data.channel, event.data);
return;
}
});
/**
* @param {HTMLDocument?} document
* @param {HTMLElement?} body

View file

@ -9,7 +9,7 @@
const sw = /** @type {ServiceWorkerGlobalScope} */ (/** @type {any} */ (self));
const VERSION = 3;
const VERSION = 4;
const resourceCacheName = `vscode-resource-cache-${VERSION}`;
@ -127,33 +127,25 @@ const notFound = () =>
const methodNotAllowed = () =>
new Response('Method Not Allowed', { status: 405, });
const vscodeMessageChannel = new MessageChannel();
sw.addEventListener('message', event => {
sw.addEventListener('message', async (event) => {
switch (event.data.channel) {
case 'init':
case 'version':
{
const source = /** @type {Client} */ (event.source);
sw.clients.get(source.id).then(client => {
client?.postMessage({
channel: 'init',
version: VERSION
}, [vscodeMessageChannel.port2]);
if (client) {
client.postMessage({
channel: 'version',
version: VERSION
});
}
});
return;
}
default:
console.log('Unknown message');
return;
}
});
vscodeMessageChannel.port1.onmessage = (event) => {
switch (event.data.channel) {
case 'did-load-resource':
{
/** @type {ResourceResponse} */
const response = event.data;
const response = event.data.data;
if (!resourceRequestStore.resolve(response.id, response)) {
console.log('Could not resolve unknown resource', response.path);
}
@ -161,7 +153,7 @@ vscodeMessageChannel.port1.onmessage = (event) => {
}
case 'did-load-localhost':
{
const data = event.data;
const data = event.data.data;
if (!localhostRequestStore.resolve(data.id, data.location)) {
console.log('Could not resolve unknown localhost', data.origin);
}
@ -171,7 +163,7 @@ vscodeMessageChannel.port1.onmessage = (event) => {
console.log('Unknown message');
return;
}
};
});
sw.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
@ -193,7 +185,7 @@ sw.addEventListener('fetch', (event) => {
});
sw.addEventListener('install', (event) => {
event.waitUntil(sw.skipWaiting());
event.waitUntil(sw.skipWaiting()); // Activate worker immediately
});
sw.addEventListener('activate', (event) => {
@ -205,6 +197,18 @@ sw.addEventListener('activate', (event) => {
* @param {URL} requestUrl
*/
async function processResourceRequest(event, requestUrl) {
const client = await sw.clients.get(event.clientId);
if (!client) {
console.error('Could not find inner client for request');
return notFound();
}
const webviewId = getWebviewIdForClient(client);
if (!webviewId) {
console.error('Could not resolve webview id');
return notFound();
}
const shouldTryCaching = (event.request.method === 'GET');
/**
@ -254,6 +258,12 @@ async function processResourceRequest(event, requestUrl) {
return response.clone();
};
const parentClients = await getOuterIframeClient(webviewId);
if (!parentClients.length) {
console.log('Could not find parent client for request');
return notFound();
}
/** @type {Response | undefined} */
let cached;
if (shouldTryCaching) {
@ -267,15 +277,17 @@ async function processResourceRequest(event, requestUrl) {
const scheme = firstHostSegment.split('+', 1)[0];
const authority = firstHostSegment.slice(scheme.length + 1); // may be empty
vscodeMessageChannel.port1.postMessage({
channel: 'load-resource',
id: requestId,
path: requestUrl.pathname,
scheme,
authority,
query: requestUrl.search.replace(/^\?/, ''),
ifNoneMatch: cached?.headers.get('ETag'),
});
for (const parentClient of parentClients) {
parentClient.postMessage({
channel: 'load-resource',
id: requestId,
path: requestUrl.pathname,
scheme,
authority,
query: requestUrl.search.replace(/^\?/, ''),
ifNoneMatch: cached?.headers.get('ETag'),
});
}
return promise.then(entry => resolveResourceEntry(entry, cached));
}
@ -292,6 +304,11 @@ async function processLocalhostRequest(event, requestUrl) {
// that are not spawned by vs code
return fetch(event.request);
}
const webviewId = getWebviewIdForClient(client);
if (!webviewId) {
console.error('Could not resolve webview id');
return fetch(event.request);
}
const origin = requestUrl.origin;
@ -312,13 +329,42 @@ async function processLocalhostRequest(event, requestUrl) {
});
};
const { requestId, promise } = localhostRequestStore.create();
const parentClients = await getOuterIframeClient(webviewId);
if (!parentClients.length) {
console.log('Could not find parent client for request');
return notFound();
}
vscodeMessageChannel.port1.postMessage({
channel: 'load-localhost',
origin: origin,
id: requestId,
});
const { requestId, promise } = localhostRequestStore.create();
for (const parentClient of parentClients) {
parentClient.postMessage({
channel: 'load-localhost',
origin: origin,
id: requestId,
});
}
return promise.then(resolveRedirect);
}
/**
* @param {Client} client
* @returns {string | null}
*/
function getWebviewIdForClient(client) {
const requesterClientUrl = new URL(client.url);
return requesterClientUrl.searchParams.get('id');
}
/**
* @param {string} webviewId
* @returns {Promise<Client[]>}
*/
async function getOuterIframeClient(webviewId) {
const allClients = await sw.clients.matchAll({ includeUncontrolled: true });
return allClients.filter(client => {
const clientUrl = new URL(client.url);
const hasExpectedPathName = (clientUrl.pathname === `${rootPath}/` || clientUrl.pathname === `${rootPath}/index.html`);
return hasExpectedPathName && clientUrl.searchParams.get('id') === webviewId;
});
}

View file

@ -48,6 +48,8 @@ export const enum WebviewMessageChannels {
doUpdateState = 'do-update-state',
doReload = 'do-reload',
setConfirmBeforeClose = 'set-confirm-before-close',
loadResource = 'load-resource',
loadLocalhost = 'load-localhost',
webviewReady = 'webview-ready',
wheel = 'did-scroll-wheel',
fatalError = 'fatal-error',
@ -55,29 +57,8 @@ export const enum WebviewMessageChannels {
didKeydown = 'did-keydown',
didKeyup = 'did-keyup',
didContextMenu = 'did-context-menu',
// Service worker
initServiceWorker = 'init-service-worker',
didInitServiceWorker = 'did-init-service-worker',
}
const enum ServiceWorkerMessages {
// From
loadResource = 'load-resource',
loadLocalhost = 'load-localhost',
// To
didLoadLocalHost = 'did-load-localhost',
didLoadResource = 'did-load-resource',
}
type ResponseResponse =
| { readonly status: 200; id: number; path: string; mime: string; data: Uint8Array; etag: string | undefined; mtime: number | undefined; } // success
| { readonly status: 304; id: number; path: string; mime: string; mtime: number | undefined } // not modified
| { readonly status: 401; id: number; path: string } // unauthorized
| { readonly status: 404; id: number; path: string } // not found
;
interface IKeydownEvent {
key: string;
keyCode: number;
@ -118,7 +99,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
protected get platform(): string { return 'browser'; }
private readonly _expectedServiceWorkerVersion = 3; // Keep this in sync with the version in service-worker.js
private readonly _expectedServiceWorkerVersion = 4; // Keep this in sync with the version in service-worker.js
private _element: HTMLIFrameElement | undefined;
protected get element(): HTMLIFrameElement | undefined { return this._element; }
@ -317,64 +298,28 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
});
}));
this._register(this.on(WebviewMessageChannels.initServiceWorker, (_, e) => {
const swPort = e.ports[0];
swPort.onmessage = async (e) => {
switch (e.data.channel) {
case ServiceWorkerMessages.loadResource:
{
const entry = e.data;
try {
// Restore the authority we previously encoded
const authority = decodeAuthority(entry.authority);
const uri = URI.from({
scheme: entry.scheme,
authority: authority,
path: decodeURIComponent(entry.path), // This gets re-encoded
query: entry.query ? decodeURIComponent(entry.query) : entry.query,
});
this._register(this.on(WebviewMessageChannels.loadResource, async (entry: { id: number, path: string, query: string, scheme: string, authority: string, ifNoneMatch?: string }) => {
try {
// Restore the authority we previously encoded
const authority = decodeAuthority(entry.authority);
const uri = URI.from({
scheme: entry.scheme,
authority: authority,
path: decodeURIComponent(entry.path), // This gets re-encoded
query: entry.query ? decodeURIComponent(entry.query) : entry.query,
});
this.loadResource(entry.id, uri, entry.ifNoneMatch);
} catch (e) {
this._send('did-load-resource', {
id: entry.id,
status: 404,
path: entry.path,
});
}
}));
const body = await this.loadResource(entry.id, uri, entry.ifNoneMatch);
return swPort.postMessage({
channel: ServiceWorkerMessages.didLoadResource,
...body
});
} catch (e) {
return swPort.postMessage({
channel: ServiceWorkerMessages.didLoadResource,
id: entry.id,
status: 404,
path: entry.path,
});
}
}
case ServiceWorkerMessages.loadLocalhost:
{
const entry = e.data;
try {
const redirect = await this.getLocalhostRedirect(entry.id, entry.origin);
return swPort.postMessage({
channel: ServiceWorkerMessages.didLoadLocalHost,
id: entry.id,
origin: entry.origin,
location: redirect
});
} catch (e) {
return swPort.postMessage({
channel: ServiceWorkerMessages.didLoadLocalHost,
id: entry.id,
origin: entry.origin,
location: undefined
});
}
}
default:
console.log('Unknown message received in renderer process from service worker port');
return;
}
};
this._send(WebviewMessageChannels.didInitServiceWorker);
this._register(this.on(WebviewMessageChannels.loadLocalhost, (entry: any) => {
this.localLocalhost(entry.id, entry.origin);
}));
this._register(Event.runAndSubscribe(webviewThemeDataProvider.onThemeDataChanged, () => this.style()));
@ -756,7 +701,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
}
}
private async loadResource(id: number, uri: URI, ifNoneMatch: string | undefined): Promise<ResponseResponse> {
private async loadResource(id: number, uri: URI, ifNoneMatch: string | undefined) {
try {
const result = await loadLocalResource(uri, {
ifNoneMatch,
@ -766,7 +711,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
switch (result.type) {
case WebviewResourceResponse.Type.Success: {
const { buffer } = await streamToBuffer(result.stream);
return {
return this._send('did-load-resource', {
id,
status: 200,
path: uri.path,
@ -774,39 +719,45 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
data: buffer,
etag: result.etag,
mtime: result.mtime
};
});
}
case WebviewResourceResponse.Type.NotModified:
return {
case WebviewResourceResponse.Type.NotModified: {
return this._send('did-load-resource', {
id,
status: 304, // not modified
path: uri.path,
mime: result.mimeType,
mtime: result.mtime
};
case WebviewResourceResponse.Type.AccessDenied:
return {
});
}
case WebviewResourceResponse.Type.AccessDenied: {
return this._send('did-load-resource', {
id,
status: 401, // unauthorized
path: uri.path,
};
});
}
}
} catch {
// noop
}
return {
return this._send('did-load-resource', {
id,
status: 404,
path: uri.path,
};
});
}
private async getLocalhostRedirect(id: string, origin: string) {
private async localLocalhost(id: string, origin: string) {
const authority = this._environmentService.remoteAuthority;
const resolveAuthority = authority ? await this._remoteAuthorityResolverService.resolveAuthority(authority) : undefined;
return resolveAuthority ? this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined;
const redirect = resolveAuthority ? await this._portMappingManager.getRedirect(resolveAuthority.authority, origin) : undefined;
return this._send('did-load-localhost', {
id,
origin,
location: redirect
});
}
public focus(): void {

View file

@ -235,7 +235,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
const webviewExternalEndpointCommit = this.payload?.get('webviewExternalEndpointCommit');
return endpoint
.replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? '69df0500a8963fc469161c038a14a39384d5a303')
.replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? 'd372f9187401bd145a0a6e15ba369e2d82d02005')
.replace('{{quality}}', (webviewExternalEndpointCommit ? 'insider' : this.productService.quality) ?? 'insider');
}

View file

@ -282,7 +282,7 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) {
}
});
const payloadParam = `[["enableProposedApi",""],["webviewExternalEndpointCommit","69df0500a8963fc469161c038a14a39384d5a303"],["skipWelcome","true"]]`;
const payloadParam = `[["enableProposedApi",""],["webviewExternalEndpointCommit","d372f9187401bd145a0a6e15ba369e2d82d02005"],["skipWelcome","true"]]`;
await measureAndLog(page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${URI.file(workspacePath!).path}&payload=${payloadParam}`), 'page.goto()', logger);
return { browser, context, page };

View file

@ -65,7 +65,7 @@ async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWith
const testExtensionUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionDevelopmentPath)).path, protocol, host, slashes: true });
const testFilesUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionTestsPath)).path, protocol, host, slashes: true });
const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","69df0500a8963fc469161c038a14a39384d5a303"],["skipWelcome","true"]]`;
const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","d372f9187401bd145a0a6e15ba369e2d82d02005"],["skipWelcome","true"]]`;
if (path.extname(testWorkspaceUri) === '.code-workspace') {
await page.goto(`${endpoint.href}&workspace=${testWorkspaceUri}&payload=${payloadParam}`);