From f3d72ca6f45daa6327c9ef88f8018caa0af900d0 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sun, 12 Nov 2023 11:50:49 +0100 Subject: [PATCH] iframe widget pooling (experimental) --- plugs/index/mentions_ps.ts | 1 - plugs/markdown/assets/markdown_widget.css | 12 +- plugs/markdown/markdown_content_widget.ts | 2 - web/components/panel_html.ts | 10 + web/components/widget_sandbox_iframe.ts | 265 ++++++++++++++-------- 5 files changed, 188 insertions(+), 102 deletions(-) diff --git a/plugs/index/mentions_ps.ts b/plugs/index/mentions_ps.ts index 638ebc9a..5fdd2be5 100644 --- a/plugs/index/mentions_ps.ts +++ b/plugs/index/mentions_ps.ts @@ -57,7 +57,6 @@ export async function renderMentions() { "ps", 1, ` -
diff --git a/plugs/markdown/assets/markdown_widget.css b/plugs/markdown/assets/markdown_widget.css index ce58b9ca..dcf23f5b 100644 --- a/plugs/markdown/assets/markdown_widget.css +++ b/plugs/markdown/assets/markdown_widget.css @@ -1,9 +1,8 @@ /* Reset SB styles */ -html, + body { - height: initial !important; - overflow: initial !important; - background-color: var(--root-background-color); + font-family: var(--editor-font); + color: var(--root-color); } #sb-main { @@ -26,11 +25,6 @@ body { height: initial !important; } -body { - font-family: var(--editor-font); - color: var(--root-color); -} - ul, ol { margin-top: 0; diff --git a/plugs/markdown/markdown_content_widget.ts b/plugs/markdown/markdown_content_widget.ts index 06176cf0..854cdd27 100644 --- a/plugs/markdown/markdown_content_widget.ts +++ b/plugs/markdown/markdown_content_widget.ts @@ -33,8 +33,6 @@ export async function wrapHTML(html: string): Promise { const css = await asset.readAsset("assets/markdown_widget.css"); return ` - - diff --git a/web/components/panel_html.ts b/web/components/panel_html.ts index ab53ed85..df98bb21 100644 --- a/web/components/panel_html.ts +++ b/web/components/panel_html.ts @@ -99,6 +99,16 @@ function loadJsByUrl(url) { }); } + + + diff --git a/web/components/widget_sandbox_iframe.ts b/web/components/widget_sandbox_iframe.ts index 4d5b5b2b..97f6d536 100644 --- a/web/components/widget_sandbox_iframe.ts +++ b/web/components/widget_sandbox_iframe.ts @@ -2,100 +2,185 @@ import { WidgetContent } from "$sb/app_event.ts"; import { Client } from "../client.ts"; import { panelHtml } from "./panel_html.ts"; +/** + * Implements sandbox widgets using iframe with a pooling mechanism to speed up loading + */ + +type PreloadedIFrame = { + // The wrapped iframe element + iframe: HTMLIFrameElement; + // Has the iframe been used yet? + used: boolean; + // Is it ready (that is: has the initial load happened) + ready: Promise; +}; + +const iframePool = new Set(); +const desiredPoolSize = 5; +const gcInterval = 5000; + +function populateIFramePool() { + while (iframePool.size < desiredPoolSize) { + iframePool.add(prepareSandboxIFrame()); + } +} + +populateIFramePool(); + +setInterval(() => { + // Iterate over all iframes + for (const preloadedIframe of iframePool) { + if ( + // Is this iframe in use, but has it since been removed from the DOM? + preloadedIframe.used && !document.body.contains(preloadedIframe.iframe) + ) { + // Ditch it + console.log("Garbage collecting iframe", preloadedIframe); + iframePool.delete(preloadedIframe); + } + } + populateIFramePool(); +}, gcInterval); + +export function prepareSandboxIFrame(): PreloadedIFrame { + console.log("Preloading iframe"); + const iframe = document.createElement("iframe"); + iframe.src = "about:blank"; + + const ready = new Promise((resolve) => { + iframe.onload = () => { + iframe.contentDocument!.write(panelHtml); + resolve(); + }; + }); + return { + iframe, + used: false, + ready, + }; +} + +function claimIFrame(): PreloadedIFrame { + for (const preloadedIframe of iframePool) { + if (!preloadedIframe.used) { + console.log("Took iframe from pool"); + preloadedIframe.used = true; + return preloadedIframe; + } + } + // Nothing available in the pool, let's spin up a new one and add it to the pool + const newPreloadedIFrame = prepareSandboxIFrame(); + newPreloadedIFrame.used = true; + iframePool.add(newPreloadedIFrame); + return newPreloadedIFrame; +} + +export function mountIFrame( + preloadedIFrame: PreloadedIFrame, + client: Client, + widgetHeightCacheKey: string | null, + content: WidgetContent | Promise, + onMessage?: (message: any) => void, +) { + const iframe = preloadedIFrame.iframe; + preloadedIFrame.ready.then(async () => { + const messageListener = (evt: any) => { + (async () => { + if (evt.source !== iframe.contentWindow) { + return; + } + const data = evt.data; + if (!data) { + return; + } + switch (data.type) { + case "syscall": { + const { id, name, args } = data; + try { + const result = await client.system.localSyscall(name, args); + if (!iframe.contentWindow) { + // iFrame already went away + return; + } + iframe.contentWindow!.postMessage({ + type: "syscall-response", + id, + result, + }); + } catch (e: any) { + if (!iframe.contentWindow) { + // iFrame already went away + return; + } + iframe.contentWindow!.postMessage({ + type: "syscall-response", + id, + error: e.message, + }); + } + break; + } + case "setHeight": + iframe.style.height = data.height + "px"; + if (widgetHeightCacheKey) { + client.space.setCachedWidgetHeight( + widgetHeightCacheKey, + data.height, + ); + } + break; + default: + if (onMessage) { + onMessage(data); + } + } + })().catch((e) => { + console.error("Message listener error", e); + }); + }; + + // Subscribe to message event on global object (to receive messages from iframe) + globalThis.addEventListener("message", messageListener); + // Only run this code once + iframe.onload = null; + const resolvedContent = await Promise.resolve(content); + if (!iframe.contentWindow) { + console.warn("Iframe went away or content was not loaded"); + return; + } + if (resolvedContent.html) { + iframe.contentWindow!.postMessage({ + type: "html", + html: resolvedContent.html, + script: resolvedContent.script, + theme: document.getElementsByTagName("html")[0].dataset.theme, + }); + } else if (resolvedContent.url) { + iframe.contentWindow!.location.href = resolvedContent.url; + if (resolvedContent.height) { + iframe.style.height = resolvedContent.height + "px"; + } + if (resolvedContent.width) { + iframe.style.width = resolvedContent.width + "px"; + } + } + }).catch(console.error); +} + export function createWidgetSandboxIFrame( client: Client, widgetHeightCacheKey: string | null, content: WidgetContent | Promise, onMessage?: (message: any) => void, ) { - const iframe = document.createElement("iframe"); - iframe.src = "about:blank"; - - const messageListener = (evt: any) => { - (async () => { - if (evt.source !== iframe.contentWindow) { - return; - } - const data = evt.data; - if (!data) { - return; - } - switch (data.type) { - case "syscall": { - const { id, name, args } = data; - try { - const result = await client.system.localSyscall(name, args); - if (!iframe.contentWindow) { - // iFrame already went away - return; - } - iframe.contentWindow!.postMessage({ - type: "syscall-response", - id, - result, - }); - } catch (e: any) { - if (!iframe.contentWindow) { - // iFrame already went away - return; - } - iframe.contentWindow!.postMessage({ - type: "syscall-response", - id, - error: e.message, - }); - } - break; - } - case "setHeight": - iframe.style.height = data.height + "px"; - if (widgetHeightCacheKey) { - client.space.setCachedWidgetHeight( - widgetHeightCacheKey, - data.height, - ); - } - break; - default: - if (onMessage) { - onMessage(data); - } - } - })().catch((e) => { - console.error("Message listener error", e); - }); - }; - - iframe.onload = () => { - iframe.contentDocument!.write(panelHtml); - - // Subscribe to message event on global object (to receive messages from iframe) - globalThis.addEventListener("message", messageListener); - // Only run this code once - iframe.onload = null; - Promise.resolve(content).then((content) => { - if (!iframe.contentWindow) { - console.warn("Iframe went away or content was not loaded"); - return; - } - if (content.html) { - iframe.contentWindow!.postMessage({ - type: "html", - html: content.html, - script: content.script, - theme: document.getElementsByTagName("html")[0].dataset.theme, - }); - } else if (content.url) { - iframe.contentWindow!.location.href = content.url; - if (content.height) { - iframe.style.height = content.height + "px"; - } - if (content.width) { - iframe.style.width = content.width + "px"; - } - } - }).catch(console.error); - }; - - return iframe; + // console.log("Claiming iframe"); + const preloadedIFrame = claimIFrame(); + mountIFrame( + preloadedIFrame, + client, + widgetHeightCacheKey, + content, + onMessage, + ); + return preloadedIFrame.iframe; }