Skip to content

Commit

Permalink
Progress monitoring: Use a custom instantiateWasm handler to avoid mo…
Browse files Browse the repository at this point in the history
…nkey-patching WebAssembly.instantiateStreaming (#1305)

Monitors the `.wasm` file download through a custom `instantiateWasm`
handler passed to the Emscripten module.

Before this PR, Playground "decorated" WebAssembly.instantiateStreaming
to monitor the stream progress. This introduced additional complexity in
[Loopback Request
support](#1287)
and so this PR replaces it with a less hacky solution.

 ## Testing instructions

Confirm the E2E tests pass
  • Loading branch information
adamziel authored Apr 23, 2024
1 parent d9a5e1a commit 34d612b
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 48 deletions.
31 changes: 0 additions & 31 deletions packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ export class EmscriptenDownloadMonitor extends EventTarget {
#assetsSizes: Record<string, number> = {};
#progress: Record<string, number> = {};

constructor() {
super();

this.#monitorWebAssemblyStreaming();
}

expectAssets(assets: Record<string, number>) {
for (const [urlLike, size] of Object.entries(assets)) {
const dummyBaseUrl = 'http://example.com/';
Expand All @@ -60,31 +54,6 @@ export class EmscriptenDownloadMonitor extends EventTarget {
return cloneResponseMonitorProgress(response, onProgress);
}

/**
* Replaces the default WebAssembly.instantiateStreaming with a version
* that monitors the download #progress.
*/
#monitorWebAssemblyStreaming() {
const instantiateStreaming = WebAssembly.instantiateStreaming;
WebAssembly.instantiateStreaming = async (
responseOrPromise,
...args
) => {
const response = await responseOrPromise;
const file = response.url.substring(
new URL(response.url).origin.length + 1
);

const reportingResponse = cloneResponseMonitorProgress(
response,
({ detail: { loaded, total } }) =>
this.#notify(file, loaded, total)
);

return instantiateStreaming(reportingResponse, ...args);
};
}

/**
* Notifies about the download #progress of a file.
*
Expand Down
7 changes: 7 additions & 0 deletions packages/php-wasm/universal/src/lib/load-php-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ export type EmscriptenOptions = {
onRuntimeInitialized?: () => void;
monitorRunDependencies?: (left: number) => void;
onMessage?: (listener: EmscriptenMessageListener) => void;
instantiateWasm?: (
info: WebAssembly.Imports,
receiveInstance: (
instance: WebAssembly.Instance,
module: WebAssembly.Module
) => void
) => void;
} & Record<string, any>;

export type EmscriptenMessageListener = (type: string, data: string) => void;
11 changes: 3 additions & 8 deletions packages/php-wasm/web/src/lib/web-php.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import {
BasePHP,
DataModule,
EmscriptenOptions,
loadPHPRuntime,
PHPLoaderModule,
PHPRequestHandlerConfiguration,
SupportedPHPVersion,
} from '@php-wasm/universal';
import { EmscriptenDownloadMonitor } from '@php-wasm/progress';
import { getPHPLoaderModule } from './get-php-loader-module';

export interface PHPWebLoaderOptions {
emscriptenOptions?: EmscriptenOptions;
downloadMonitor?: EmscriptenDownloadMonitor;
requestHandler?: PHPRequestHandlerConfiguration;
dataModules?: Array<DataModule | Promise<DataModule>>;
onPhpLoaderModuleLoaded?: (module: PHPLoaderModule) => void;
/** @deprecated To be replaced with `extensions` in the future */
loadAllExtensions?: boolean;
}
Expand Down Expand Up @@ -75,10 +73,7 @@ export class WebPHP extends BasePHP {
const variant = options.loadAllExtensions ? 'kitchen-sink' : 'light';

const phpLoaderModule = await getPHPLoaderModule(phpVersion, variant);
options.downloadMonitor?.expectAssets({
[phpLoaderModule.dependencyFilename]:
phpLoaderModule.dependenciesTotalSize,
});
options.onPhpLoaderModuleLoaded?.(phpLoaderModule);
return await loadPHPRuntime(phpLoaderModule, {
...(options.emscriptenOptions || {}),
...fakeWebsocket(),
Expand Down
49 changes: 40 additions & 9 deletions packages/playground/remote/src/lib/worker-thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,24 @@ if (

const scope = Math.random().toFixed(16);
const scopedSiteUrl = setURLScope(wordPressSiteUrl, scope).toString();
const monitor = new EmscriptenDownloadMonitor();
const downloadMonitor = new EmscriptenDownloadMonitor();

const monitoredFetch = (input: RequestInfo | URL, init?: RequestInit) =>
downloadMonitor.monitorFetch(fetch(input, init));

// Start downloading WordPress if needed
let wordPressRequest = null;
if (!wordPressAvailableInOPFS) {
if (requestedWPVersion.startsWith('http')) {
// We don't know the size upfront, but we can still monitor the download.
// monitorFetch will read the content-length response header when available.
wordPressRequest = monitor.monitorFetch(fetch(requestedWPVersion));
wordPressRequest = monitoredFetch(requestedWPVersion);
} else {
const wpDetails = getWordPressModuleDetails(wpVersion);
monitor.expectAssets({
downloadMonitor.expectAssets({
[wpDetails.url]: wpDetails.size,
});
wordPressRequest = monitor.monitorFetch(fetch(wpDetails.url));
wordPressRequest = monitoredFetch(wpDetails.url);
}
}

Expand All @@ -124,17 +127,39 @@ const php = new WebPHP(undefined, {
rewriteRules: wordPressRewriteRules,
});

const recreateRuntime = async () =>
await WebPHP.loadRuntime(phpVersion, {
downloadMonitor: monitor,
const recreateRuntime = async () => {
let wasmUrl = '';
return await WebPHP.loadRuntime(phpVersion, {
onPhpLoaderModuleLoaded: (phpLoaderModule) => {
wasmUrl = phpLoaderModule.dependencyFilename;
downloadMonitor.expectAssets({
[wasmUrl]: phpLoaderModule.dependenciesTotalSize,
});
},
// We don't yet support loading specific PHP extensions one-by-one.
// Let's just indicate whether we want to load all of them.
loadAllExtensions: phpExtensions?.length > 0,
requestHandler: {
rewriteRules: wordPressRewriteRules,
},
emscriptenOptions: {
instantiateWasm(imports, receiveInstance) {
// Using .then because Emscripten typically returns an empty
// object here and not a promise.
monitoredFetch(wasmUrl, {
credentials: 'same-origin',
})
.then((response) =>
WebAssembly.instantiateStreaming(response, imports)
)
.then((wasm) => {
receiveInstance(wasm.instance, wasm.module);
});
return {};
},
},
});

};
// Rotate the PHP runtime periodically to avoid memory leak-related crashes.
// @see https://github.com/WordPress/wordpress-playground/pull/990 for more context
rotatePHPRuntime({
Expand Down Expand Up @@ -236,7 +261,13 @@ export class PlaygroundWorkerEndpoint extends WebPHPEndpoint {
}

const [setApiReady, setAPIError] = exposeAPI(
new PlaygroundWorkerEndpoint(php, monitor, scope, wpVersion, phpVersion)
new PlaygroundWorkerEndpoint(
php,
downloadMonitor,
scope,
wpVersion,
phpVersion
)
);

try {
Expand Down

0 comments on commit 34d612b

Please sign in to comment.