Skip to content

Commit a67cb5b

Browse files
[PHP] Support multiple workers for NODEFS /wordpress mounts – Asyncify (#2317)
## Overview Adds multi-worker support for Node.js Asyncify builds to complement [JSPI support](#2231) and enable usage in Node < v23. Note this doesn't work with Asyncify in web browsers. This PR also adds `fileLockManager` support for `@php-wasm/cli` ## Implementation This PR is different from other Asyncify PRs in that it doesn't actually make things work with Asyncify. Instead, it switches to synchronous message passing when JSPI is unavailable. This way, the Asyncify builds never have to engage in stack switching around `fd_close()` or `fcntl()`. This wasn't the first choice, but getting the Asyncify builds right was just too challenging, so we had to use another approach. ### Synchronous message passing via Comlink > [!IMPORTANT] > This PR forks the Comlink library to add `afterResponseSent?: (ev: MessageEvent) => void` argument to the `expose()` function. The rest is unchanged. Comlink isn't getting many new PRs so skipping updates (or backporting occasionally) seems fine. Playground already uses Comlink for RPC between workers. This PR adds synchronous bindings via `exposeSync` and `wrapSync`. The specific technique of exchanging messages is described in https://github.com/adamziel/js-synchronous-messaging: - Worker A calls `postMessage()` on Worker B's MessagePort to start the RPC exchange. - Worker A uses [Atomics.wait](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait) (with a `SharedArrayBuffer`) to synchronously wait for a notification from the Worker B. - Worker A uses [receiveMessageOnPort](https://nodejs.org/api/worker_threads.html#workerreceivemessageonportport) to synchronously read the data sent by Worker B. For usage example, see [comlink-sync.spec.ts](https://github.com/WordPress/wordpress-playground/blob/042387f57846a046b98e5837956111f53fa862fa/packages/php-wasm/universal/src/lib/comlink-sync.spec.ts#L87). The upsides of this approach: * Saves dozens-to-hundreds of hours on debugging Asyncify issues * Increased reliability * Provides useful stack traces when errors do happen. The downsides: * Fragmentation: Both synchronous and asynchronous handlers exist to get the best our of both Asyncify and JSPI. * Node.js-only: This extension does not implement a Safari-friendly transport. SharedArrayBuffer is an option, but it requires more restrictive CORP+COEP headers which breaks, e.g., YouTube embeds. Synchronous XHR might work if we really need Safari support for one of the new asynchronous features, but other than that let's just skip adding new asynchronous WASM features to Safari until WebKit supports stack switching. * Message passing between workers is slow. Avoid using synchronous messaging for syscalls that are invoked frequently and handled asynchronously in the same worker. ### Dual channel support The Emscripten-built php.js requires either: * A synchronous or asynchronous `fileLockManager` – when JSPI is available * A synchronous `fileLockManager` – when JSPI is not available This is implemented with preprocessor directives, e.g. ``` #if ASYNCIFY == 2 return Asyncify.handleAsync(async () => { #endif // ..code.. #if ASYNCIFY == 2 }); #endif ``` Why support both methods and not always use synchronous calls? Because web browsers have no `receiveMessageOnPort` and can only handle asynchronous message passing. Supporting both sync and async message channels provides maximum compatibility. The only environment where `fileLockManager` is not supported is Safari. ## Testing Instructions (or ideally a Blueprint) ### Playground CLI Run Playground CLI server with 5 workers using Asyncify: ```shell node --disable-warning=ExperimentalWarning --experimental-strip-types --experimental-transform-types --import ./packages/meta/src/node-es-module-loader/register.mts ./packages/playground/cli/src/cli.ts server --experimental-multi-worker=5 --mount-before-install ./tmp/new-site:/wordpress ``` Create some posts, install some plugins, confirm it does not crash. Then do the same with JSPI and confirm everything continues to work: ```shell node --experimental-wasm-jspi --disable-warning=ExperimentalWarning --experimental-strip-types --experimental-transform-types --import ./packages/meta/src/node-es-module-loader/register.mts ./packages/playground/cli/src/cli.ts server --experimental-multi-worker=5 --mount-before-install ./tmp/new-site:/wordpress ``` ### PHP.wasm CLI Run the test script below and confirm it does not crash or corrupt the database: ```shell node --loader=./packages/meta/src/node-es-module -loader/loader.mts ./packages/php-wasm/cli/src/main.ts test.php ``` test.php ```php <?php $db = new SQLite3('./db.sqlite'); $db->exec('CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL, category TEXT NOT NULL )'); // Insert some product data $db->exec("INSERT INTO products (name, price, category) VALUES ('Laptop', 999.99, 'Electronics')"); $db->exec("INSERT INTO products (name, price, category) VALUES ('Coffee Mug', 12.50, 'Kitchen')"); $db->exec("INSERT INTO products (name, price, category) VALUES ('Notebook', 5.99, 'Office')"); $db->exec("INSERT INTO products (name, price, category) VALUES ('Headphones', 79.99, 'Electronics')"); $result = $db->query('SELECT * FROM products ORDER BY category, name'); while ($row = $result->fetchArray(SQLITE3_ASSOC)) { echo "- {$row['name']} ({$row['category']}): $" . number_format($row['price'], 2) . "\n"; } $db->close(); ``` --------- Co-authored-by: Adam Zieliński <[email protected]>
1 parent 8c8f1f9 commit a67cb5b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+247814
-38239
lines changed

package-lock.json

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@
7272
"ajv": "8.12.0",
7373
"async-lock": "1.4.1",
7474
"classnames": "^2.3.2",
75-
"comlink": "^4.4.2",
7675
"crc-32": "1.2.2",
7776
"diff3": "0.0.4",
7877
"express": "4.21.2",

packages/php-wasm/cli/src/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@php-wasm/universal';
1212
import type { SupportedPHPVersion } from '@php-wasm/universal';
1313

14+
import { FileLockManagerForNode } from '@php-wasm/node';
1415
import { PHP } from '@php-wasm/universal';
1516
import { loadNodeRuntime, useHostFilesystem } from '@php-wasm/node';
1617
import path from 'path';
@@ -76,6 +77,8 @@ ${process.argv[0]} ${process.execArgv.join(' ')} ${process.argv[1]}
7677
const php = new PHP(
7778
await loadNodeRuntime(phpVersion, {
7879
emscriptenOptions: {
80+
fileLockManager: new FileLockManagerForNode(),
81+
processId: 1,
7982
ENV: {
8083
...envVariables,
8184
TMPDIR: sysTempDir,
@@ -109,7 +112,7 @@ ${process.argv[0]} ${process.execArgv.join(' ')} ${process.argv[1]}
109112
})
110113
);
111114

112-
response.exitCode
115+
await response.exitCode
113116
.catch((result) => {
114117
if (result.name === 'ExitStatus') {
115118
process.exit(result.status === undefined ? 1 : result.status);

packages/php-wasm/compile/php/Dockerfile

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,10 @@ RUN set -euxo pipefail; \
523523
export PHP_VERSION_ESCAPED="${PHP_VERSION//./_}"; \
524524
echo -n " -fdebug-compilation-dir=${DEBUG_DWARF_COMPILATION_DIR}/ " \
525525
"-fdebug-prefix-map=/root/php_wasm.c=${DEBUG_DWARF_COMPILATION_DIR}/compile/php/php_wasm.c " \
526-
"-fdebug-prefix-map=/root/php-src/=${OUTPUT_DIR_ON_HOST}/${PHP_VERSION_ESCAPED}/php-src/ " \
527-
"-fdebug-prefix-map=./=${OUTPUT_DIR_ON_HOST}/${PHP_VERSION_ESCAPED}/php-src/ " \
526+
"-fdebug-prefix-map=/root/php-src/=${OUTPUT_DIR_ON_HOST}/${PHP_VERSION_ESCAPED}/php-src/ " \
527+
"-fdebug-prefix-map=./=${OUTPUT_DIR_ON_HOST}/${PHP_VERSION_ESCAPED}/php-src/ " \
528+
"-fdebug-prefix-map=/emsdk/emscripten/==${OUTPUT_DIR_ON_HOST}/${PHP_VERSION_ESCAPED}/emscripten/ " \
529+
"-fdebug-prefix-map=/root/emsdk/upstream/emscripten/=${OUTPUT_DIR_ON_HOST}/${PHP_VERSION_ESCAPED}/emscripten/ " \
528530
>> /root/.emcc-php-wasm-flags; \
529531
if [ "${WITH_SOURCEMAPS}" = "yes" ]; then \
530532
echo -n ' -gsource-map' >> /root/.emcc-php-wasm-flags; \
@@ -615,11 +617,21 @@ RUN export ASYNCIFY_IMPORTS=$'[\n\
615617
"_js_popen_to_file",\n\
616618
"_asyncjs__js_popen_to_file",\n\
617619
"__syscall_fcntl64",\n\
620+
"___syscall_fcntl64",\n\
621+
"_asyncjs___syscall_fcntl64",\n\
618622
"js_release_file_locks",\n\
623+
"_js_release_file_locks",\n\
624+
"_async_js_release_file_locks",\n\
619625
"js_flock",\n\
626+
"_js_flock",\n\
627+
"_async_js_flock",\n\
620628
"js_fd_read",\n\
621629
"_js_fd_read",\n\
630+
"fd_close",\n\
622631
"_fd_close",\n\
632+
"_asyncjs__fd_close",\n\
633+
"close",\n\
634+
"_close",\n\
623635
"js_module_onMessage",\n\
624636
"_js_module_onMessage",\n\
625637
"_asyncjs__js_module_onMessage",\n\
@@ -632,7 +644,15 @@ RUN export ASYNCIFY_IMPORTS=$'[\n\
632644
"_wasm_shutdown",\n\
633645
"_asyncjs__wasm_shutdown"]'; \
634646
echo -n " -s ASYNCIFY_IMPORTS=$ASYNCIFY_IMPORTS " | tr -d "\n" >> /root/.emcc-php-asyncify-flags; \
635-
export ASYNCIFY_ONLY_UNPREFIXED=$'"__fseeko_unlocked",\
647+
export ASYNCIFY_ONLY_UNPREFIXED=$'"null.<anonymous>",\
648+
"__stdio_close",\
649+
"zend_stream_stdio_closer",\
650+
"zend_destroy_file_handle",\
651+
"php_init_config",\
652+
"zend_register_ini_entries_ex",\
653+
"php_module_startup",\
654+
"wasm_sapi_module_startup",\
655+
"__fseeko_unlocked",\
636656
"__ftello_unlocked",\
637657
"__funcs_on_exit",\
638658
"__fwritex",\
@@ -849,6 +869,9 @@ RUN export ASYNCIFY_IMPORTS=$'[\n\
849869
"zif_stream_select",\
850870
"_php_stream_fill_read_buffer",\
851871
"_php_stream_read",\
872+
"php_sqlite3_object_free_storage",\
873+
"php_sqlite3_error",\
874+
"sqlite3_reset",\
852875
"php_stream_read_to_str",\
853876
"php_userstreamop_read",\
854877
"zif_fread",\
@@ -858,6 +881,7 @@ RUN export ASYNCIFY_IMPORTS=$'[\n\
858881
"zif_fwrite",\
859882
"php_stdiop_write",\
860883
"zif_array_filter",\
884+
"zend_unclean_zval_ptr_dtor",\
861885
"zend_call_known_instance_method_with_2_params",\
862886
"zend_fetch_dimension_address_read_R",\
863887
"_zval_dtor_func_for_ptr",\
@@ -1656,6 +1680,7 @@ RUN export ASYNCIFY_IMPORTS=$'[\n\
16561680
"zend_hash_reverse_apply",\
16571681
"ZEND_INCLUDE_OR_EVAL_SPEC_CV_HANDLER",\
16581682
"zend_include_or_eval",\
1683+
"compile_filename",\
16591684
"zend_internal_type_error",\
16601685
"zend_interrupt_helper_SPEC",\
16611686
"zend_invalid_class_constant_type_error",\
@@ -1965,6 +1990,14 @@ RUN export ASYNCIFY_IMPORTS=$'[\n\
19651990
"zim_sqlite3_backup",\
19661991
"zim_SQLite3_backup",\
19671992
"zim_SQLite3_exec",\
1993+
"zim_sqlite3_exec",\
1994+
"zim_sqlite3_query",\
1995+
"zim_sqlite3_querySingle",\
1996+
"zim_sqlite3result_fetchArray",\
1997+
"php_cli_startup",\
1998+
"php_stdiop_close",\
1999+
"zend_parse_ini_file",\
2000+
"php_embed_startup",\
19682001
"zip_source_file_common_new",\
19692002
"zip_source_function_create",\
19702003
"zip_source_layered_create",\
@@ -2062,7 +2095,7 @@ RUN set -euxo pipefail; \
20622095
export OPTIMIZATION_FLAGS="-O0"; \
20632096
fi; \
20642097
PLATFORM_SPECIFIC_ARGS=''; \
2065-
if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ] && [ "$WITH_JSPI" = "yes" ]; then \
2098+
if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
20662099
PLATFORM_SPECIFIC_ARGS="$PLATFORM_SPECIFIC_ARGS -DPHP_WASM_FILE_LOCKING_SUPPORT=1"; \
20672100
PLATFORM_SPECIFIC_ARGS="$PLATFORM_SPECIFIC_ARGS --js-library /root/phpwasm-emscripten-library-dynamic-linking.js"; \
20682101
PLATFORM_SPECIFIC_ARGS="$PLATFORM_SPECIFIC_ARGS --js-library /root/phpwasm-emscripten-library-file-locking-for-node.js -Wl,--wrap=getpid"; \
@@ -2240,6 +2273,9 @@ RUN set -euxo pipefail; \
22402273
if [ "${WITH_SOURCEMAPS}" = "yes" ] || [ "${WITH_DEBUG}" = "yes" ]; then \
22412274
# Make PHP source available for use with step debugger
22422275
rm -rf /root/php-src/.git; \
2243-
cp -r /root/php-src "/root/output/${PHP_VERSION_ESCAPED}/php-src"; \
2244-
cp -r /root/emsdk/upstream/emscripten/system "/root/output/${PHP_VERSION_ESCAPED}/php-src/system"; \
2276+
cp -r /root/php-src /root/output/"${PHP_VERSION_ESCAPED}"/php-src; \
2277+
# Copy emscripten source files
2278+
mkdir -p /root/output/"${PHP_VERSION_ESCAPED}"/emscripten; \
2279+
cp -r /root/emsdk/upstream/emscripten/cache /root/output/"${PHP_VERSION_ESCAPED}"/emscripten/; \
2280+
cp -r /root/emsdk/upstream/emscripten/system /root/output/"${PHP_VERSION_ESCAPED}"/emscripten/; \
22452281
fi;

0 commit comments

Comments
 (0)