Skip to content

Commit

Permalink
Multiple worker backends on the web (to fix the Chrome crash) (WordPr…
Browse files Browse the repository at this point in the history
…ess#28)

Load WASM in an iframe instead of a Webworker to work around a Google Chrome out of memory error.

This commit enables defering the WASM workload to a configurable backend. It ships with three backends: `iframeWorkerBackend`, `webWorkerBackend`, `sharedWorkerBackend`. The `iframeWorkerBackend` is the default.

See WordPress#1 for more details about the Google Chrome crash.

**Important!** The iframe must be loaded from another domain to spin a new browser thread. If it's loaded from the same domain, WordPress "server" will run in the same thread that paints the user interface and dramatically slow down all user interactions.
  • Loading branch information
adamziel authored Oct 11, 2022
1 parent 75337c4 commit eb7b6b9
Show file tree
Hide file tree
Showing 22 changed files with 628 additions and 306 deletions.
145 changes: 98 additions & 47 deletions dist-web/app.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,117 @@
(() => {
// src/shared/messaging.mjs
function postMessageFactory(target) {
let lastRequestId = 0;
return function postMessage(data, timeout = 5e4) {
return new Promise((resolve, reject) => {
const requestId = ++lastRequestId;
const responseHandler = (event) => {
if (event.data.type === "response" && event.data.requestId === requestId) {
target.removeEventListener("message", responseHandler);
clearTimeout(failOntimeout);
resolve(event.data.result);
}
};
const failOntimeout = setTimeout(() => {
reject("Request timed out");
target.removeEventListener("message", responseHandler);
}, timeout);
target.addEventListener("message", responseHandler);
target.postMessage({
...data,
requestId
});
});
};
var DEFAULT_REPLY_TIMEOUT = 25e3;
var lastMessageId = 0;
function postMessageExpectReply(messageTarget, message, ...postMessageArgs) {
const messageId = ++lastMessageId;
messageTarget.postMessage(
{
...message,
messageId
},
...postMessageArgs
);
return messageId;
}
async function awaitReply(messageTarget, messageId, timeout = DEFAULT_REPLY_TIMEOUT) {
return new Promise((resolve, reject) => {
const responseHandler = (event) => {
if (event.data.type === "response" && event.data.messageId === messageId) {
messageTarget.removeEventListener("message", responseHandler);
clearTimeout(failOntimeout);
resolve(event.data.result);
}
};
const failOntimeout = setTimeout(() => {
reject(new Error("Request timed out"));
messageTarget.removeEventListener("message", responseHandler);
}, timeout);
messageTarget.addEventListener("message", responseHandler);
});
}
function replyTo(event, result, target) {
target.postMessage({
function responseTo(messageId, result) {
return {
type: "response",
requestId: event.data.requestId,
messageId,
result
}, event.origin);
};
}

// src/web/app.mjs
if (!navigator.serviceWorker) {
alert("Service workers are not supported by your browser");
}
var serviceWorkerReady = navigator.serviceWorker.register(`/service-worker.js`);
var myWebWorker = new Worker("web-worker.js");
var webWorkerReady = new Promise((resolve) => {
const callback = (event) => {
if (event.data.type === "ready") {
resolve();
myWebWorker.removeEventListener("message", callback);
var serviceWorkerChannel = new BroadcastChannel("wordpress-service-worker");
serviceWorkerChannel.addEventListener("message", async function onMessage(event) {
console.debug(`[Main] "${event.data.type}" message received from a service worker`);
let result;
if (event.data.type === "is_ready") {
result = isReady;
} else if (event.data.type === "request" || event.data.type === "httpRequest") {
const worker = await wasmWorker;
result = await worker.HTTPRequest(event.data.request);
}
if (event.data.messageId) {
serviceWorkerChannel.postMessage(
responseTo(
event.data.messageId,
result
)
);
}
console.debug(`[Main] "${event.data.type}" message processed`, { result });
});
var wasmWorker = createWordPressWorker(
{
backend: iframeWorkerBackend("http://127.0.0.1:8778/iframe-worker.html"),
wordPressSiteURL: location.href
}
);
async function createWordPressWorker({ backend, wordPressSiteURL }) {
while (true) {
try {
await backend.sendMessage({ type: "is_alive" }, 50);
break;
} catch (e) {
}
await new Promise((resolve) => setTimeout(resolve, 50));
}
await backend.sendMessage({
type: "initialize_wordpress",
siteURL: wordPressSiteURL
});
return {
async HTTPRequest(request) {
return await backend.sendMessage({
type: "request",
request
});
}
};
myWebWorker.addEventListener("message", callback);
});
}
function iframeWorkerBackend(workerDocumentURL) {
const iframe = document.createElement("iframe");
iframe.src = workerDocumentURL;
iframe.style.display = "none";
document.body.appendChild(iframe);
return {
sendMessage: async function(message, timeout = DEFAULT_REPLY_TIMEOUT) {
const messageId = postMessageExpectReply(iframe.contentWindow, message, "*");
const response = await awaitReply(window, messageId, timeout);
return response;
}
};
}
var isReady = false;
async function init() {
console.log("[Main] Initializing the worker");
await wasmWorker;
await serviceWorkerReady;
await webWorkerReady;
const postMessage = postMessageFactory(myWebWorker);
window.addEventListener("message", async (event) => {
if (event.data.type === "goto") {
document.querySelector("iframe").src = event.data.path;
}
console.log("[APP.js] Got a message", event);
const response = await postMessage(event.data);
console.log("[APP.js] Got a response", response);
replyTo(event, response, parent);
});
document.querySelector("iframe").src = "/wp-login.php";
isReady = true;
console.log("[Main] Iframe is ready");
const WPIframe = document.querySelector("#wp");
WPIframe.src = "/wp-login.php";
}
init();
})();
9 changes: 9 additions & 0 deletions dist-web/iframe-worker.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body style="padding: 0; margin: 0">
<script src="wasm-worker.js"></script>
</body>
</html>

2 changes: 1 addition & 1 deletion dist-web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title>WordPress code embed!</title>
</head>
<body style="padding: 0; margin: 0">
<iframe style="width: 100vw; height: 100vh; border: 0; margin: 0; padding: 0;"></iframe>
<iframe id="wp" style="width: 100vw; height: 100vh; border: 0; margin: 0; padding: 0;"></iframe>
<script src="app.js"></script>
</body>
</html>
Expand Down
22 changes: 22 additions & 0 deletions dist-web/php-web.js

Large diffs are not rendered by default.

Binary file not shown.
22 changes: 22 additions & 0 deletions dist-web/php-webworker.js

Large diffs are not rendered by default.

66 changes: 37 additions & 29 deletions dist-web/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
(() => {
// src/shared/messaging.mjs
function postMessageFactory(target) {
let lastRequestId = 0;
return function postMessage(data, timeout = 5e4) {
return new Promise((resolve, reject) => {
const requestId = ++lastRequestId;
const responseHandler = (event) => {
if (event.data.type === "response" && event.data.requestId === requestId) {
target.removeEventListener("message", responseHandler);
clearTimeout(failOntimeout);
resolve(event.data.result);
}
};
const failOntimeout = setTimeout(() => {
reject("Request timed out");
target.removeEventListener("message", responseHandler);
}, timeout);
target.addEventListener("message", responseHandler);
target.postMessage({
...data,
requestId
});
});
};
var DEFAULT_REPLY_TIMEOUT = 25e3;
var lastMessageId = 0;
function postMessageExpectReply(messageTarget, message, ...postMessageArgs) {
const messageId = ++lastMessageId;
messageTarget.postMessage(
{
...message,
messageId
},
...postMessageArgs
);
return messageId;
}
async function awaitReply(messageTarget, messageId, timeout = DEFAULT_REPLY_TIMEOUT) {
return new Promise((resolve, reject) => {
const responseHandler = (event) => {
if (event.data.type === "response" && event.data.messageId === messageId) {
messageTarget.removeEventListener("message", responseHandler);
clearTimeout(failOntimeout);
resolve(event.data.result);
}
};
const failOntimeout = setTimeout(() => {
reject(new Error("Request timed out"));
messageTarget.removeEventListener("message", responseHandler);
}, timeout);
messageTarget.addEventListener("message", responseHandler);
});
}

// src/web/service-worker.js
var workerChannel = new BroadcastChannel("wordpress-service-worker");
var postWebWorkerMessage = postMessageFactory(workerChannel);
var broadcastChannel = new BroadcastChannel("wordpress-service-worker");
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
const isWpOrgRequest = url.hostname.includes("api.wordpress.org");
const isPHPRequest = url.pathname.endsWith("/") || url.pathname.endsWith(".php");
const isPHPRequest = url.pathname.endsWith("/") && url.pathname !== "/" || url.pathname.endsWith(".php");
if (isWpOrgRequest || !isPHPRequest) {
console.log(`[ServiceWorker] Ignoring request: ${url.pathname}`);
return;
Expand All @@ -40,23 +44,27 @@
return event.respondWith(
new Promise(async (accept) => {
console.log(`[ServiceWorker] Serving request: ${url.pathname}?${url.search}`);
console.log({ isWpOrgRequest, isPHPRequest });
const post = await parsePost(event.request);
const requestHeaders = {};
for (const pair of event.request.headers.entries()) {
requestHeaders[pair[0]] = pair[1];
}
let wpResponse;
try {
wpResponse = await postWebWorkerMessage({
const message = {
type: "httpRequest",
request: {
path: url.pathname + url.search,
method: event.request.method,
_POST: post,
headers: requestHeaders
}
});
console.log({ wpResponse });
};
console.log("[ServiceWorker] Forwarding a request to the main app", { message });
const messageId = postMessageExpectReply(broadcastChannel, message);
wpResponse = await awaitReply(broadcastChannel, messageId);
console.log("[ServiceWorker] Response received from the main app", { wpResponse });
} catch (e) {
console.error(e);
throw e;
Expand Down
Loading

0 comments on commit eb7b6b9

Please sign in to comment.