Repository: flutter/flutter
Pull request #164748: [skwasm] Dynamic Threading
Author: login: eyebrowsoffire
Labels: name: engine
description: flutter/engine related. See also e: labels.
name: platform-web
description: Web applications specifically
Created: 2025-03-06T23:05:10Z | Merged: 2025-03-10T21:52:54Z

This switches skwasm over from the emscripten pthreads implementation to emscripten's "wasm workers" threading implementation. The pthreads implementation simply will not run at all in a non-crossOriginIsolated context, but the wasm workers implementation only fails if we actually attempt to spawn a thread. This means we can actually choose whether to use a single-threaded or multi-threaded strategy at runtime, which means we don't have to build two variants of skwasm for single- vs multi-threaded.


Diff:
diff --git a/engine/src/build/config/compiler/BUILD.gn b/engine/src/build/config/compiler/BUILD.gn
index 15d61c09c0f2d..de15cb053e460 100644
--- a/engine/src/build/config/compiler/BUILD.gn
+++ b/engine/src/build/config/compiler/BUILD.gn
@@ -277,9 +277,9 @@ config("compiler") {
   }
 
   if (is_wasm) {
-    if (wasm_use_pthreads) {
-      cflags += [ "-pthread" ]
-      ldflags += [ "-pthread" ]
+    if (wasm_use_workers) {
+      cflags += [ "-sWASM_WORKERS=1" ]
+      ldflags += [ "-sWASM_WORKERS=1" ]
     } if (wasm_shared_memory) {
       cflags += [ "-sSHARED_MEMORY=1" ]
       ldflags += [ "-sSHARED_MEMORY=1" ]
@@ -396,12 +396,12 @@ config("compiler") {
     # to say that it does. Define them here instead.
     defines += [ "HAVE_SYS_UIO_H" ]
 
-    # When Android requires new flags consider also editing the flags in 
-    # the following locations. 
+    # When Android requires new flags consider also editing the flags in
+    # the following locations.
     # Framework plugin_ffi template: packages/flutter_tools/templates/plugin_ffi/src.tmpl/CMakeLists.txt.tmpl
     # Example PR: https://github.com/flutter/flutter/pull/155508
-    # Dart Lang JNI package: pkgs/jni/src/CMakeLists.txt 
-    # Example PR: https://github.com/dart-lang/native/pull/1615 
+    # Dart Lang JNI package: pkgs/jni/src/CMakeLists.txt
+    # Example PR: https://github.com/dart-lang/native/pull/1615
     ldflags += [
       "-Wl,--no-undefined",
       "-Wl,--exclude-libs,ALL",
diff --git a/engine/src/build/toolchain/wasm.gni b/engine/src/build/toolchain/wasm.gni
index 0da69d2874e11..e2515b7f3b188 100644
--- a/engine/src/build/toolchain/wasm.gni
+++ b/engine/src/build/toolchain/wasm.gni
@@ -11,7 +11,7 @@ declare_args() {
   # The location of an activated embedded emsdk.
   emsdk_dir = rebase_path("//flutter/prebuilts/emsdk")
 
-  wasm_use_pthreads = false
+  wasm_use_workers = false
   wasm_shared_memory = false
   wasm_use_dwarf = false
 }
diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter
index a595ac041d9d2..9ea932ee9449d 100644
--- a/engine/src/flutter/ci/licenses_golden/licenses_flutter
+++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter
@@ -42845,8 +42845,6 @@ ORIGIN: ../../../flutter/lib/web_ui/skwasm/filters.cpp + ../../../flutter/LICENS
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/fonts.cpp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/helpers.h + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/image.cpp + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/lib/web_ui/skwasm/library_skwasm_multi_threaded.js + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/lib/web_ui/skwasm/library_skwasm_single_threaded.js + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/library_skwasm_support.js + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/paint.cpp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/path.cpp + ../../../flutter/LICENSE
@@ -42856,8 +42854,6 @@ ORIGIN: ../../../flutter/lib/web_ui/skwasm/skwasm_support.h + ../../../flutter/L
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/string.cpp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/surface.cpp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/surface.h + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/lib/web_ui/skwasm/surface_mt.cpp + ../../../flutter/LICENSE
-ORIGIN: ../../../flutter/lib/web_ui/skwasm/surface_st.cpp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/line_metrics.cpp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/paragraph.cpp + ../../../flutter/LICENSE
 ORIGIN: ../../../flutter/lib/web_ui/skwasm/text/paragraph_builder.cpp + ../../../flutter/LICENSE
@@ -45820,8 +45816,6 @@ FILE: ../../../flutter/lib/web_ui/skwasm/filters.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/fonts.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/helpers.h
 FILE: ../../../flutter/lib/web_ui/skwasm/image.cpp
-FILE: ../../../flutter/lib/web_ui/skwasm/library_skwasm_multi_threaded.js
-FILE: ../../../flutter/lib/web_ui/skwasm/library_skwasm_single_threaded.js
 FILE: ../../../flutter/lib/web_ui/skwasm/library_skwasm_support.js
 FILE: ../../../flutter/lib/web_ui/skwasm/paint.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/path.cpp
@@ -45831,8 +45825,6 @@ FILE: ../../../flutter/lib/web_ui/skwasm/skwasm_support.h
 FILE: ../../../flutter/lib/web_ui/skwasm/string.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/surface.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/surface.h
-FILE: ../../../flutter/lib/web_ui/skwasm/surface_mt.cpp
-FILE: ../../../flutter/lib/web_ui/skwasm/surface_st.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/text/line_metrics.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/text/paragraph.cpp
 FILE: ../../../flutter/lib/web_ui/skwasm/text/paragraph_builder.cpp
diff --git a/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart b/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart
index 9537110afc5d1..a70ca968f17dd 100644
--- a/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart
+++ b/engine/src/flutter/lib/web_ui/dev/steps/copy_artifacts_step.dart
@@ -76,7 +76,6 @@ class CopyArtifactsStep implements PipelineStep {
     final String canvaskitSourceDirectory;
     final String canvaskitChromiumSourceDirectory;
     final String skwasmSourceDirectory;
-    final String skwasmStSourceDirectory;
     switch (source) {
       case LocalArtifactSource(:final mode):
         final buildDirectory = getBuildDirectoryForRuntimeMode(mode).path;
@@ -84,7 +83,6 @@ class CopyArtifactsStep implements PipelineStep {
         canvaskitSourceDirectory = pathlib.join(buildDirectory, 'canvaskit');
         canvaskitChromiumSourceDirectory = pathlib.join(buildDirectory, 'canvaskit_chromium');
         skwasmSourceDirectory = pathlib.join(buildDirectory, 'skwasm');
-        skwasmStSourceDirectory = pathlib.join(buildDirectory, 'skwasm_st');
 
       case GcsArtifactSource(:final realm):
         final artifactsDirectory = (await _downloadArtifacts(realm)).path;
@@ -96,7 +94,6 @@ class CopyArtifactsStep implements PipelineStep {
           'chromium',
         );
         skwasmSourceDirectory = pathlib.join(artifactsDirectory, 'canvaskit');
-        skwasmStSourceDirectory = pathlib.join(artifactsDirectory, 'canvaskit');
     }
 
     await environment.webTestsArtifactsDir.create(recursive: true);
@@ -116,7 +113,6 @@ class CopyArtifactsStep implements PipelineStep {
     if (artifactDeps.skwasm) {
       copied.add('Skwasm');
       await copyWasmLibrary('skwasm', skwasmSourceDirectory, 'canvaskit');
-      await copyWasmLibrary('skwasm_st', skwasmStSourceDirectory, 'canvaskit');
     }
     print('Copied artifacts: ${copied.join(', ')}');
   }
diff --git a/engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js b/engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
index 8a72f1cd77c9e..311999776c41a 100644
--- a/engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
+++ b/engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
@@ -6,24 +6,64 @@ import { createWasmInstantiator } from "./instantiate_wasm.js";
 import { resolveUrlWithSegments } from "./utils.js";
 
 export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => {
-  const fileStem = (browserEnvironment.crossOriginIsolated && !config.forceSingleThreadedSkwasm) ? "skwasm" : "skwasm_st";
-  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, `${fileStem}.js`)
+  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, 'skwasm.js')
   let skwasmUrl = rawSkwasmUrl;
   if (deps.flutterTT.policy) {
     skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
   }
-  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, `${fileStem}.wasm`));
+  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, 'skwasm.wasm'));
   const skwasm = await import(skwasmUrl);
   return await skwasm.default({
+    skwasmSingleThreaded: !browserEnvironment.crossOriginIsolated || config.forceSingleThreadedSkwasm,
     instantiateWasm: wasmInstantiator,
-    // When hosted via a CDN or some other url that is not the same
-    // origin as the main script of the page, we will fail to create
-    // a web worker with the bootstrapping script. This workaround will
-    // make sure that the worker JS can be loaded regardless of where
-    // it is hosted.
-    mainScriptUrlOrBlob: new Blob(
-      [`import '${skwasmUrl}'`],
-      { 'type': 'application/javascript' },
-    ),
+    locateFile: (filename, scriptDirectory) => {
+      // The wasm workers API has a separate .ww.js file that bootstraps the
+      // web worker. However, it turns out this worker bootstrapper doesn't
+      // actually work with ES6 modules, which we have enabled. So we instead
+      // pass our own bootstrapper that loads skwasm.js as an ES6 module, and
+      // queues/flushes pending messages that were received during the
+      // asynchronous load.
+      if (filename.endsWith('.ww.js')) {
+        const url = resolveUrlWithSegments(baseUrl, filename);
+        return URL.createObjectURL(new Blob(
+          [`
+"use strict";
+
+let eventListener;
+eventListener = (message) => {
+    const pendingMessages = [];
+    const data = message.data;
+    data["instantiateWasm"] = (info,receiveInstance) => {
+        const instance = new WebAssembly.Instance(data["wasm"], info);
+        return receiveInstance(instance, data["wasm"])
+    };
+    import(data.js).then(async (skwasm) => {
+        await skwasm.default(data);
+
+        removeEventListener("message", eventListener);
+        for (const message of pendingMessages) {
+            dispatchEvent(message);
+        }
+    });
+    console.log("removing initial listener");
+    removeEventListener("message", eventListener);
+    eventListener = (message) => {
+
+        pendingMessages.push(message);
+    };
+
+    addEventListener("message", eventListener);
+};
+addEventListener("message", eventListener);
+`
+          ],
+          { 'type': 'application/javascript' }));
+      }
+      return url;
+    },
+    // Because of the above workaround, the worker is just a blob and
+    // can't locate the main script using a relative path to itself,
+    // so we pass the main script location in.
+    mainScriptUrlOrBlob: rawSkwasmUrl,
   });
 }
diff --git a/engine/src/flutter/lib/web_ui/skwasm/BUILD.gn b/engine/src/flutter/lib/web_ui/skwasm/BUILD.gn
index 2d9063d5f59b3..1e4221cf71a64 100644
--- a/engine/src/flutter/lib/web_ui/skwasm/BUILD.gn
+++ b/engine/src/flutter/lib/web_ui/skwasm/BUILD.gn
@@ -4,96 +4,69 @@
 
 import("//build/toolchain/wasm.gni")
 
-template("skwasm_variant") {
-  wasm_lib(target_name) {
-    public_configs = [ "//flutter:config" ]
+wasm_lib("skwasm") {
+  public_configs = [ "//flutter:config" ]
 
-    sources = [
-      "canvas.cpp",
-      "contour_measure.cpp",
-      "data.cpp",
-      "export.h",
-      "filters.cpp",
-      "fonts.cpp",
-      "helpers.h",
-      "image.cpp",
-      "paint.cpp",
-      "path.cpp",
-      "picture.cpp",
-      "shaders.cpp",
-      "skwasm_support.h",
-      "string.cpp",
-      "surface.cpp",
-      "text/line_metrics.cpp",
-      "text/paragraph.cpp",
-      "text/paragraph_builder.cpp",
-      "text/paragraph_style.cpp",
-      "text/strut_style.cpp",
-      "text/text_style.cpp",
-      "vertices.cpp",
-      "wrappers.h",
-    ]
-
-    cflags = [ "-mreference-types" ]
-
-    ldflags = [
-      "-std=c++20",
-      "-lGL",
-      "-sUSE_WEBGL2=1",
-      "-sMAX_WEBGL_VERSION=2",
-      "-sOFFSCREENCANVAS_SUPPORT",
-      "-sALLOW_MEMORY_GROWTH",
-      "-sALLOW_TABLE_GROWTH",
-      "-lexports.js",
-      "-sEXPORTED_FUNCTIONS=[stackAlloc]",
-      "-sEXPORTED_RUNTIME_METHODS=[addFunction,wasmExports,wasmMemory,stackAlloc]",
-      "-sINCOMING_MODULE_JS_API=[instantiateWasm,noExitRuntime,mainScriptUrlOrBlob]",
-      "-sUSE_ES6_IMPORT_META=0",
-      "--js-library",
-      rebase_path("library_skwasm_support.js"),
-    ]
+  sources = [
+    "canvas.cpp",
+    "contour_measure.cpp",
+    "data.cpp",
+    "export.h",
+    "filters.cpp",
+    "fonts.cpp",
+    "helpers.h",
+    "image.cpp",
+    "paint.cpp",
+    "path.cpp",
+    "picture.cpp",
+    "shaders.cpp",
+    "skwasm_support.h",
+    "string.cpp",
+    "surface.cpp",
+    "text/line_metrics.cpp",
+    "text/paragraph.cpp",
+    "text/paragraph_builder.cpp",
+    "text/paragraph_style.cpp",
+    "text/strut_style.cpp",
+    "text/text_style.cpp",
+    "vertices.cpp",
+    "wrappers.h",
+  ]
 
-    inputs = [ rebase_path("library_skwasm_support.js") ]
+  cflags = [ "-mreference-types" ]
 
-    if (invoker.multi_threaded) {
-      sources += [ "surface_mt.cpp" ]
-      ldflags += [
-        "-sPTHREAD_POOL_SIZE=1",
-        "-Wno-pthreads-mem-growth",
-        "--js-library",
-        rebase_path("library_skwasm_multi_threaded.js"),
-      ]
-      inputs += [ rebase_path("library_skwasm_multi_threaded.js") ]
-    } else {
-      sources += [ "surface_st.cpp" ]
-      ldflags += [
-        "--js-library",
-        rebase_path("library_skwasm_single_threaded.js"),
-      ]
-      inputs += [ rebase_path("library_skwasm_single_threaded.js") ]
-    }
+  ldflags = [
+    "-std=c++20",
+    "-lGL",
+    "-sUSE_WEBGL2=1",
+    "-sMAX_WEBGL_VERSION=2",
+    "-sOFFSCREENCANVAS_SUPPORT",
+    "-sALLOW_MEMORY_GROWTH",
+    "-sALLOW_TABLE_GROWTH",
+    "-lexports.js",
+    "-sEXPORTED_FUNCTIONS=[stackAlloc]",
+    "-sEXPORTED_RUNTIME_METHODS=[addFunction,wasmExports,wasmMemory,stackAlloc]",
+    "-sINCOMING_MODULE_JS_API=[instantiateWasm,locateFile,noExitRuntime,mainScriptUrlOrBlob,wasmMemory,wasm,skwasmSingleThreaded]",
+    "-sUSE_ES6_IMPORT_META=0",
+    "--js-library",
+    rebase_path("library_skwasm_support.js"),
+  ]
 
-    if (is_debug) {
-      ldflags += [
-        "-sASSERTIONS=1",
-        "-sGL_ASSERTIONS=1",
-      ]
-    } else {
-      ldflags += [ "--closure=1" ]
-    }
+  inputs = [ rebase_path("library_skwasm_support.js") ]
 
-    deps = [
-      "//flutter/skia",
-      "//flutter/skia/modules/skparagraph",
-      "//flutter/skia/modules/skunicode",
+  if (is_debug) {
+    ldflags += [
+      "-sASSERTIONS=1",
+      "-sGL_ASSERTIONS=1",
+      "-sSTACK_OVERFLOW_CHECK=2",
     ]
+  } else {
+    ldflags += [ "--closure=1" ]
   }
-}
-
-skwasm_variant("skwasm") {
-  multi_threaded = true
-}
 
-skwasm_variant("skwasm_st") {
-  multi_threaded = false
+  deps = [
+    "//flutter/skia",
+    "//flutter/skia/modules/skparagraph",
+    "//flutter/skia/modules/skunicode",
+  ]
 }
diff --git a/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_multi_threaded.js b/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_multi_threaded.js
deleted file mode 100644
index 237ccf38bed10..0000000000000
--- a/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_multi_threaded.js
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// This file adds JavaScript APIs that are accessible to the C++ layer.
-// See: https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#implement-a-c-api-in-javascript
-
-mergeInto(LibraryManager.library, {
-  $skwasm_threading_setup__postset: 'skwasm_threading_setup();',
-  $skwasm_threading_setup: function() {
-    // This value represents the difference between the time origin of the main
-    // thread and whichever web worker this code is running on. This is so that
-    // when we report frame timings, that they are in the same time domain
-    // regardless of whether they are captured on the main thread or the web
-    // worker.
-    let timeOriginDelta = 0;
-    skwasm_registerMessageListener = function(threadId, listener) {
-      const eventListener = function({data}) {
-        const skwasmMessage = data.skwasmMessage;
-        if (!skwasmMessage) {
-          return;
-        }
-        if (skwasmMessage == 'syncTimeOrigin') {
-          timeOriginDelta = performance.timeOrigin - data.timeOrigin;
-          return;
-        }
-        listener(data);
-      };
-      if (!threadId) {
-        addEventListener("message", eventListener);
-      } else {
-        PThread.pthreads[threadId].addEventListener("message", eventListener);
-        PThread.pthreads[threadId].postMessage({
-          skwasmMessage: 'syncTimeOrigin',
-          timeOrigin: performance.timeOrigin,
-        });    
-      }
-    };
-    skwasm_getCurrentTimestamp = function() {
-      return performance.now() + timeOriginDelta;
-    };
-    skwasm_postMessage = function(message, transfers, threadId) {
-      if (threadId) {
-        PThread.pthreads[threadId].postMessage(message, transfers);
-      } else {
-        postMessage(message, transfers);
-      }
-    };
-  },
-  $skwasm_threading_setup__deps: ['$skwasm_registerMessageListener', '$skwasm_getCurrentTimestamp', '$skwasm_postMessage'],
-  $skwasm_registerMessageListener: function() {},
-  $skwasm_registerMessageListener__deps: ['$skwasm_threading_setup'],
-  $skwasm_getCurrentTimestamp: function () {},
-  $skwasm_getCurrentTimestamp__deps: ['$skwasm_threading_setup'],
-  $skwasm_postMessage: function () {},
-  $skwasm_postMessage__deps: ['$skwasm_threading_setup'],
-});
diff --git a/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_single_threaded.js b/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_single_threaded.js
deleted file mode 100644
index 6b6221ad8850f..0000000000000
--- a/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_single_threaded.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// This file adds JavaScript APIs that are accessible to the C++ layer.
-// See: https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#implement-a-c-api-in-javascript
-
-mergeInto(LibraryManager.library, {
-  $skwasm_threading_setup__postset: 'skwasm_threading_setup();',
-  $skwasm_threading_setup: function() {
-    let messageListener;
-    skwasm_registerMessageListener = function(threadId, listener) {
-      messageListener = listener;
-    };
-    skwasm_getCurrentTimestamp = function() {
-      return performance.now();
-    };
-    skwasm_postMessage = function(message, transfers, threadId) {
-      queueMicrotask(() => {
-        messageListener(message);
-      })
-    };
-  },
-  $skwasm_threading_setup__deps: ['$skwasm_registerMessageListener', '$skwasm_getCurrentTimestamp', '$skwasm_postMessage'],
-  $skwasm_registerMessageListener: function() {},
-  $skwasm_registerMessageListener__deps: ['$skwasm_threading_setup'],
-  $skwasm_getCurrentTimestamp: function () {},
-  $skwasm_getCurrentTimestamp__deps: ['$skwasm_threading_setup'],
-  $skwasm_postMessage: function () {},
-  $skwasm_postMessage__deps: ['$skwasm_threading_setup'],
-});
diff --git a/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_support.js b/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_support.js
index db107d668da17..702724e71d82e 100644
--- a/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_support.js
+++ b/engine/src/flutter/lib/web_ui/skwasm/library_skwasm_support.js
@@ -8,9 +8,51 @@
 mergeInto(LibraryManager.library, {
   $skwasm_support_setup__postset: 'skwasm_support_setup();',
   $skwasm_support_setup: function() {
+    // This value represents the difference between the time origin of the main
+    // thread and whichever web worker this code is running on. This is so that
+    // when we report frame timings, that they are in the same time domain
+    // regardless of whether they are captured on the main thread or the web
+    // worker.
+    let timeOriginDelta = 0;
+    skwasm_registerMessageListener = function(threadId, listener) {
+      const eventListener = function({data}) {
+        const skwasmMessage = data.skwasmMessage;
+        if (!skwasmMessage) {
+          return;
+        }
+        if (skwasmMessage == 'syncTimeOrigin') {
+          timeOriginDelta = performance.timeOrigin - data.timeOrigin;
+          return;
+        }
+        listener(data);
+      };
+      if (!threadId) {
+        addEventListener("message", eventListener);
+      } else {
+        _wasmWorkers[threadId].addEventListener("message", eventListener);
+        _wasmWorkers[threadId].postMessage({
+          skwasmMessage: 'syncTimeOrigin',
+          timeOrigin: performance.timeOrigin,
+        });
+      }
+    };
+    skwasm_getCurrentTimestamp = function() {
+      return performance.now() + timeOriginDelta;
+    };
+    skwasm_postMessage = function(message, transfers, threadId) {
+      if (threadId) {
+        _wasmWorkers[threadId].postMessage(message, transfers);
+      } else {
+        postMessage(message, transfers);
+      }
+    };
+
     const handleToCanvasMap = new Map();
     const associatedObjectsMap = new Map();
 
+    _skwasm_isSingleThreaded = function() {
+      return Module["skwasmSingleThreaded"];
+    };
     _skwasm_setAssociatedObjectOnThread = function(threadId, pointer, object) {
       skwasm_postMessage({
         skwasmMessage: 'setAssociatedObject',
@@ -175,15 +217,22 @@ mergeInto(LibraryManager.library, {
       });
     }
   },
-  $skwasm_support_setup__deps: [ '$skwasm_threading_setup'],
+  $skwasm_registerMessageListener: function() {},
+  $skwasm_registerMessageListener__deps: ['$skwasm_support_setup'],
+  $skwasm_getCurrentTimestamp: function () {},
+  $skwasm_getCurrentTimestamp__deps: ['$skwasm_support_setup'],
+  $skwasm_postMessage: function () {},
+  $skwasm_postMessage__deps: ['$skwasm_support_setup'],
+  skwasm_isSingleThreaded: function() {},
+  skwasm_isSingleThreaded__deps: ['$skwasm_support_setup'],
   skwasm_setAssociatedObjectOnThread: function () {},
-  skwasm_setAssociatedObjectOnThread__deps: ['$skwasm_support_setup'],
+  skwasm_setAssociatedObjectOnThread__deps: ['$skwasm_support_setup', '$skwasm_postMessage'],
   skwasm_getAssociatedObject: function () {},
   skwasm_getAssociatedObject__deps: ['$skwasm_support_setup'],
   skwasm_disposeAssociatedObjectOnThread: function () {},
   skwasm_disposeAssociatedObjectOnThread__deps: ['$skwasm_support_setup'],
   skwasm_connectThread: function() {},
-  skwasm_connectThread__deps: ['$skwasm_support_setup'],
+  skwasm_connectThread__deps: ['$skwasm_support_setup', '$skwasm_registerMessageListener', '$skwasm_getCurrentTimestamp'],
   skwasm_dispatchRenderPictures: function() {},
   skwasm_dispatchRenderPictures__deps: ['$skwasm_support_setup'],
   skwasm_createOffscreenCanvas: function () {},
diff --git a/engine/src/flutter/lib/web_ui/skwasm/skwasm_support.h b/engine/src/flutter/lib/web_ui/skwasm/skwasm_support.h
index 57dff635b5de8..54d6e5548dbf1 100644
--- a/engine/src/flutter/lib/web_ui/skwasm/skwasm_support.h
+++ b/engine/src/flutter/lib/web_ui/skwasm/skwasm_support.h
@@ -13,6 +13,7 @@
 using SkwasmObject = __externref_t;
 
 extern "C" {
+extern bool skwasm_isSingleThreaded();
 extern void skwasm_setAssociatedObjectOnThread(unsigned long threadId,
                                                void* pointer,
                                                SkwasmObject object);
diff --git a/engine/src/flutter/lib/web_ui/skwasm/surface.cpp b/engine/src/flutter/lib/web_ui/skwasm/surface.cpp
index da0f41c9ce717..cb91b10728e82 100644
--- a/engine/src/flutter/lib/web_ui/skwasm/surface.cpp
+++ b/engine/src/flutter/lib/web_ui/skwasm/surface.cpp
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "surface.h"
+#include <emscripten/wasm_worker.h>
 #include <algorithm>
 
 #include "skwasm_support.h"
@@ -14,6 +15,23 @@
 
 using namespace Skwasm;
 
+Surface::Surface() {
+  if (skwasm_isSingleThreaded()) {
+    skwasm_connectThread(0);
+  } else {
+    assert(emscripten_is_main_browser_thread());
+
+    _thread = emscripten_malloc_wasm_worker(65536);
+    emscripten_wasm_worker_post_function_v(_thread, []() {
+      // Listen to the main thread from the worker
+      skwasm_connectThread(0);
+    });
+
+    // Listen to messages from the worker
+    skwasm_connectThread(_thread);
+  }
+}
+
 // Worker thread only
 void Surface::dispose() {
   delete this;
@@ -58,16 +76,8 @@ void Surface::setCallbackHandler(CallbackHandler* callbackHandler) {
   _callbackHandler = callbackHandler;
 }
 
-// Worker thread only
-void Surface::_runWorker() {
-  _init();
-  emscripten_exit_with_live_runtime();
-}
-
 // Worker thread only
 void Surface::_init() {
-  // Listen to messages from the main thread
-  skwasm_connectThread(0);
   _glContext = skwasm_createOffscreenCanvas(256, 256);
   if (!_glContext) {
     printf("Failed to create context!\n");
@@ -95,6 +105,8 @@ void Surface::_init() {
 
   emscripten_glGetIntegerv(GL_SAMPLES, &_sampleCount);
   emscripten_glGetIntegerv(GL_STENCIL_BITS, &_stencil);
+
+  _isInitialized = true;
 }
 
 // Worker thread only
@@ -122,6 +134,10 @@ void Surface::renderPicturesOnWorker(sk_sp<SkPicture>* pictures,
                                      int pictureCount,
                                      uint32_t callbackId,
                                      double rasterStart) {
+  if (!_isInitialized) {
+    _init();
+  }
+
   // This is populated by the `captureImageBitmap` call the first time it is
   // passed in.
   SkwasmObject imageBitmapArray = __builtin_wasm_ref_null_extern();
@@ -147,6 +163,10 @@ void Surface::renderPicturesOnWorker(sk_sp<SkPicture>* pictures,
 void Surface::rasterizeImageOnWorker(SkImage* image,
                                      ImageByteFormat format,
                                      uint32_t callbackId) {
+  if (!_isInitialized) {
+    _init();
+  }
+
   // We handle PNG encoding with browser APIs so that we can omit libpng from
   // skia to save binary size.
   assert(format != ImageByteFormat::png);
@@ -281,3 +301,7 @@ SKWASM_EXPORT void surface_onRasterizeComplete(Surface* surface,
                                                uint32_t callbackId) {
   surface->onRasterizeComplete(callbackId, data);
 }
+
+SKWASM_EXPORT bool skwasm_isMultiThreaded() {
+  return !skwasm_isSingleThreaded();
+}
diff --git a/engine/src/flutter/lib/web_ui/skwasm/surface.h b/engine/src/flutter/lib/web_ui/skwasm/surface.h
index 3ef5dbfadcfac..f793fbb2fcc33 100644
--- a/engine/src/flutter/lib/web_ui/skwasm/surface.h
+++ b/engine/src/flutter/lib/web_ui/skwasm/surface.h
@@ -75,12 +75,10 @@ class Surface {
                               uint32_t callbackId);
 
  private:
-  void _runWorker();
   void _init();
   void _resizeCanvasToFit(int width, int height);
   void _recreateSurface();
 
-  std::string _canvasID;
   CallbackHandler* _callbackHandler = nullptr;
   uint32_t _currentCallbackId = 0;
 
@@ -95,6 +93,8 @@ class Surface {
   GrGLint _stencil;
 
   pthread_t _thread;
+
+  bool _isInitialized = false;
 };
 }  // namespace Skwasm
 
diff --git a/engine/src/flutter/lib/web_ui/skwasm/surface_mt.cpp b/engine/src/flutter/lib/web_ui/skwasm/surface_mt.cpp
deleted file mode 100644
index cd02dc6d9bb36..0000000000000
--- a/engine/src/flutter/lib/web_ui/skwasm/surface_mt.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "surface.h"
-
-#include "skwasm_support.h"
-
-using namespace Skwasm;
-
-Surface::Surface() {
-  assert(emscripten_is_main_browser_thread());
-
-  pthread_attr_t attr;
-  pthread_attr_init(&attr);
-  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
-
-  pthread_create(
-      &_thread, &attr,
-      [](void* context) -> void* {
-        static_cast<Surface*>(context)->_runWorker();
-        return nullptr;
-      },
-      this);
-  // Listen to messages from the worker
-  skwasm_connectThread(_thread);
-}
-
-SKWASM_EXPORT bool skwasm_isMultiThreaded() {
-  return true;
-}
diff --git a/engine/src/flutter/lib/web_ui/skwasm/surface_st.cpp b/engine/src/flutter/lib/web_ui/skwasm/surface_st.cpp
deleted file mode 100644
index 86c7b6b22eed5..0000000000000
--- a/engine/src/flutter/lib/web_ui/skwasm/surface_st.cpp
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "surface.h"
-
-using namespace Skwasm;
-
-Surface::Surface() : _thread(0) {
-  _init();
-}
-
-SKWASM_EXPORT bool skwasm_isMultiThreaded() {
-  return false;
-}
diff --git a/engine/src/flutter/third_party/canvaskit/BUILD.gn b/engine/src/flutter/third_party/canvaskit/BUILD.gn
index 417cba35173ac..7fb119bc9e875 100644
--- a/engine/src/flutter/third_party/canvaskit/BUILD.gn
+++ b/engine/src/flutter/third_party/canvaskit/BUILD.gn
@@ -88,31 +88,7 @@ wasm_toolchain("skwasm") {
     skia_use_libpng_encode = false
 
     # skwasm is multithreaded
-    wasm_use_pthreads = true
-    wasm_prioritize_size = true
-  }
-}
-
-wasm_toolchain("skwasm_st") {
-  extra_toolchain_args = {
-    # In Chromium browsers, we can use the browser's APIs to get the necessary
-    # ICU data.
-    skia_use_icu = false
-    skia_use_client_icu = true
-    skia_icu_bidi_third_party_dir = "//flutter/third_party/canvaskit/icu_bidi"
-
-    skia_use_libjpeg_turbo_decode = false
-    skia_use_libpng_decode = false
-    skia_use_libwebp_decode = false
-
-    # We use OffscreenCanvas to produce PNG data instead of skia
-    skia_use_no_png_encode = true
-    skia_use_libpng_encode = false
-
-    # skwasm_st doesn't use pthreads, but does pass the shared memory flag in order
-    # to be compatible with the way app modules import the memory object.
-    wasm_use_pthreads = false
-    wasm_shared_memory = true
+    wasm_use_workers = true
     wasm_prioritize_size = true
   }
 }
@@ -133,20 +109,3 @@ copy("skwasm_group") {
   }
   outputs = [ "$root_out_dir/flutter_web_sdk/canvaskit/{{source_file_part}}" ]
 }
-
-copy("skwasm_st_group") {
-  visibility = [ "//flutter/web_sdk:*" ]
-  public_deps = [ "//flutter/lib/web_ui/skwasm:skwasm_st(:skwasm_st)" ]
-
-  sources = [
-    "$root_out_dir/skwasm_st/skwasm_st.js",
-    "$root_out_dir/skwasm_st/skwasm_st.js.symbols",
-    "$root_out_dir/skwasm_st/skwasm_st.wasm",
-  ]
-  if (is_debug) {
-    if (!wasm_use_dwarf) {
-      sources += [ "$root_out_dir/skwasm_st/skwasm_st.wasm.map" ]
-    }
-  }
-  outputs = [ "$root_out_dir/flutter_web_sdk/canvaskit/{{source_file_part}}" ]
-}
diff --git a/engine/src/flutter/web_sdk/BUILD.gn b/engine/src/flutter/web_sdk/BUILD.gn
index 2a1ea1350164d..8502a8b1cb3ac 100644
--- a/engine/src/flutter/web_sdk/BUILD.gn
+++ b/engine/src/flutter/web_sdk/BUILD.gn
@@ -414,7 +414,6 @@ if (!is_fuchsia) {
         "//flutter/third_party/canvaskit:canvaskit_chromium_group",
         "//flutter/third_party/canvaskit:canvaskit_group",
         "//flutter/third_party/canvaskit:skwasm_group",
-        "//flutter/third_party/canvaskit:skwasm_st_group",
       ]
     }
     deps += [ "//flutter/lib/web_ui/flutter_js" ]
@@ -440,9 +439,6 @@ if (!is_fuchsia) {
         "$root_out_dir/flutter_web_sdk/canvaskit/skwasm.js",
         "$root_out_dir/flutter_web_sdk/canvaskit/skwasm.js.symbols",
         "$root_out_dir/flutter_web_sdk/canvaskit/skwasm.wasm",
-        "$root_out_dir/flutter_web_sdk/canvaskit/skwasm_st.js",
-        "$root_out_dir/flutter_web_sdk/canvaskit/skwasm_st.js.symbols",
-        "$root_out_dir/flutter_web_sdk/canvaskit/skwasm_st.wasm",
       ]
     }
 


Reviews:
user: login: yjbanov
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T00:12:34Z
user: login: kevmoo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T00:16:39Z
user: login: eyebrowsoffire
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T01:39:49Z

Review comments:
diff_hunk: @@ -6,24 +6,67 @@ import { createWasmInstantiator } from "./instantiate_wasm.js";
 import { resolveUrlWithSegments } from "./utils.js";
 
 export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => {
-  const fileStem = (browserEnvironment.crossOriginIsolated && !config.forceSingleThreadedSkwasm) ? "skwasm" : "skwasm_st";
-  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, `${fileStem}.js`)
+  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, 'skwasm.js')
   let skwasmUrl = rawSkwasmUrl;
   if (deps.flutterTT.policy) {
     skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
   }
-  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, `${fileStem}.wasm`));
+  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, 'skwasm.wasm'));
   const skwasm = await import(skwasmUrl);
   return await skwasm.default({
+    skwasmSingleThreaded: !browserEnvironment.crossOriginIsolated || config.forceSingleThreadedSkwasm,
     instantiateWasm: wasmInstantiator,
-    // When hosted via a CDN or some other url that is not the same
-    // origin as the main script of the page, we will fail to create
-    // a web worker with the bootstrapping script. This workaround will
-    // make sure that the worker JS can be loaded regardless of where
-    // it is hosted.
-    mainScriptUrlOrBlob: new Blob(
-      [`import '${skwasmUrl}'`],
-      { 'type': 'application/javascript' },
-    ),
+    locateFile: (filename, scriptDirectory) => {
+      // The wasm workers API has a separate .ww.js file that bootstraps the
+      // web worker. However, it turns out this worker bootstrapper doesn't
+      // actually work with ES6 modules, which we have enabled. So we instead
+      // pass our own bootstrapper that loads skwasm.js as an ES6 module, and
+      // queues/flushes pending messages that were received during the
+      // asynchronous load.
+      if (filename.endsWith('.ww.js')) {
+        const url = resolveUrlWithSegments(baseUrl, filename);
+        return URL.createObjectURL(new Blob(
+          [`
+"use strict";
path: engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
user: login: yjbanov
body: I see randomly missing spaces, e.g. sometimes it's `a = b` but sometimes `a=b`, or `) => {` vs `)=>{`, etc. Is that to save over-the-wire payload size, or just hand-formatting errors?
created_at: 2025-03-07T23:46:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,24 +6,67 @@ import { createWasmInstantiator } from "./instantiate_wasm.js";
 import { resolveUrlWithSegments } from "./utils.js";
 
 export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => {
-  const fileStem = (browserEnvironment.crossOriginIsolated && !config.forceSingleThreadedSkwasm) ? "skwasm" : "skwasm_st";
-  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, `${fileStem}.js`)
+  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, 'skwasm.js')
   let skwasmUrl = rawSkwasmUrl;
   if (deps.flutterTT.policy) {
     skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
   }
-  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, `${fileStem}.wasm`));
+  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, 'skwasm.wasm'));
   const skwasm = await import(skwasmUrl);
   return await skwasm.default({
+    skwasmSingleThreaded: !browserEnvironment.crossOriginIsolated || config.forceSingleThreadedSkwasm,
     instantiateWasm: wasmInstantiator,
-    // When hosted via a CDN or some other url that is not the same
-    // origin as the main script of the page, we will fail to create
-    // a web worker with the bootstrapping script. This workaround will
-    // make sure that the worker JS can be loaded regardless of where
-    // it is hosted.
-    mainScriptUrlOrBlob: new Blob(
-      [`import '${skwasmUrl}'`],
-      { 'type': 'application/javascript' },
-    ),
+    locateFile: (filename, scriptDirectory) => {
+      // The wasm workers API has a separate .ww.js file that bootstraps the
+      // web worker. However, it turns out this worker bootstrapper doesn't
+      // actually work with ES6 modules, which we have enabled. So we instead
+      // pass our own bootstrapper that loads skwasm.js as an ES6 module, and
+      // queues/flushes pending messages that were received during the
+      // asynchronous load.
+      if (filename.endsWith('.ww.js')) {
+        const url = resolveUrlWithSegments(baseUrl, filename);
+        return URL.createObjectURL(new Blob(
+          [`
+"use strict";
+
+let eventListener;
+eventListener = (message) => {
+    const pendingMessages = [];
+    const d = message.data;
+    d["instantiateWasm"] = (info,receiveInstance) => {
+        var instance=new WebAssembly.Instance(d["wasm"],info);
path: engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
user: login: yjbanov
body: Looks like `instance` can be `const`.
created_at: 2025-03-07T23:48:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,24 +6,67 @@ import { createWasmInstantiator } from "./instantiate_wasm.js";
 import { resolveUrlWithSegments } from "./utils.js";
 
 export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => {
-  const fileStem = (browserEnvironment.crossOriginIsolated && !config.forceSingleThreadedSkwasm) ? "skwasm" : "skwasm_st";
-  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, `${fileStem}.js`)
+  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, 'skwasm.js')
   let skwasmUrl = rawSkwasmUrl;
   if (deps.flutterTT.policy) {
     skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
   }
-  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, `${fileStem}.wasm`));
+  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, 'skwasm.wasm'));
   const skwasm = await import(skwasmUrl);
   return await skwasm.default({
+    skwasmSingleThreaded: !browserEnvironment.crossOriginIsolated || config.forceSingleThreadedSkwasm,
     instantiateWasm: wasmInstantiator,
-    // When hosted via a CDN or some other url that is not the same
-    // origin as the main script of the page, we will fail to create
-    // a web worker with the bootstrapping script. This workaround will
-    // make sure that the worker JS can be loaded regardless of where
-    // it is hosted.
-    mainScriptUrlOrBlob: new Blob(
-      [`import '${skwasmUrl}'`],
-      { 'type': 'application/javascript' },
-    ),
+    locateFile: (filename, scriptDirectory) => {
+      // The wasm workers API has a separate .ww.js file that bootstraps the
+      // web worker. However, it turns out this worker bootstrapper doesn't
+      // actually work with ES6 modules, which we have enabled. So we instead
+      // pass our own bootstrapper that loads skwasm.js as an ES6 module, and
+      // queues/flushes pending messages that were received during the
+      // asynchronous load.
+      if (filename.endsWith('.ww.js')) {
+        const url = resolveUrlWithSegments(baseUrl, filename);
+        return URL.createObjectURL(new Blob(
+          [`
+"use strict";
+
+let eventListener;
+eventListener = (message) => {
+    const pendingMessages = [];
+    const d = message.data;
+    d["instantiateWasm"] = (info,receiveInstance) => {
+        var instance=new WebAssembly.Instance(d["wasm"],info);
+        return receiveInstance(instance,d["wasm"])
+    };
+    import(d.js).then(async (skwasm)=>{
+        await skwasm.default(d);
+
+        console.log("removing queueing listener");
+        removeEventListener("message", eventListener);
+        for (const message of pendingMessages) {
+            console.log("flushing message");
+            dispatchEvent(message);
+        }
+    });
+    console.log("removing initial listener");
+    removeEventListener("message", eventListener);
+    eventListener = (message) => {
+        console.log("queueing message: "+ JSON.stringify(message));
+        pendingMessages.push(message);
+    };
+    console.log("adding queuing listener");
+    addEventListener("message", eventListener);
+};
+console.log("adding initial listener");
path: engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
user: login: yjbanov
body: I'm not sure how useful the `console.log`s are to the user. This will generate a lot of noise.
created_at: 2025-03-07T23:48:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +15,23 @@
 
 using namespace Skwasm;
 
+Surface::Surface() {
+  if (skwasm_isSingleThreaded()) {
+    skwasm_connectThread(0);
+  } else {
+    assert(emscripten_is_main_browser_thread());
+
+    _thread = emscripten_malloc_wasm_worker(65536);
+    emscripten_wasm_worker_post_function_v(_thread, []() {
path: engine/src/flutter/lib/web_ui/skwasm/surface.cpp
user: login: yjbanov
body: Hmm, how does this work? Line 22 asserts `emscripten_is_main_browser_thread()`, which means the worker doesn't execute this code. If so, then the worker doesn't have this closure that's supposed to connect to the main thread. Is this some emscripten magic? 🤔 
created_at: 2025-03-08T00:11:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,24 +6,67 @@ import { createWasmInstantiator } from "./instantiate_wasm.js";
 import { resolveUrlWithSegments } from "./utils.js";
 
 export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => {
-  const fileStem = (browserEnvironment.crossOriginIsolated && !config.forceSingleThreadedSkwasm) ? "skwasm" : "skwasm_st";
-  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, `${fileStem}.js`)
+  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, 'skwasm.js')
   let skwasmUrl = rawSkwasmUrl;
   if (deps.flutterTT.policy) {
     skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
   }
-  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, `${fileStem}.wasm`));
+  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, 'skwasm.wasm'));
   const skwasm = await import(skwasmUrl);
   return await skwasm.default({
+    skwasmSingleThreaded: !browserEnvironment.crossOriginIsolated || config.forceSingleThreadedSkwasm,
     instantiateWasm: wasmInstantiator,
-    // When hosted via a CDN or some other url that is not the same
-    // origin as the main script of the page, we will fail to create
-    // a web worker with the bootstrapping script. This workaround will
-    // make sure that the worker JS can be loaded regardless of where
-    // it is hosted.
-    mainScriptUrlOrBlob: new Blob(
-      [`import '${skwasmUrl}'`],
-      { 'type': 'application/javascript' },
-    ),
+    locateFile: (filename, scriptDirectory) => {
+      // The wasm workers API has a separate .ww.js file that bootstraps the
+      // web worker. However, it turns out this worker bootstrapper doesn't
+      // actually work with ES6 modules, which we have enabled. So we instead
+      // pass our own bootstrapper that loads skwasm.js as an ES6 module, and
+      // queues/flushes pending messages that were received during the
+      // asynchronous load.
+      if (filename.endsWith('.ww.js')) {
+        const url = resolveUrlWithSegments(baseUrl, filename);
+        return URL.createObjectURL(new Blob(
+          [`
+"use strict";
+
+let eventListener;
+eventListener = (message) => {
+    const pendingMessages = [];
+    const d = message.data;
+    d["instantiateWasm"] = (info,receiveInstance) => {
+        var instance=new WebAssembly.Instance(d["wasm"],info);
+        return receiveInstance(instance,d["wasm"])
+    };
+    import(d.js).then(async (skwasm)=>{
+        await skwasm.default(d);
+
+        console.log("removing queueing listener");
+        removeEventListener("message", eventListener);
+        for (const message of pendingMessages) {
+            console.log("flushing message");
+            dispatchEvent(message);
+        }
+    });
+    console.log("removing initial listener");
+    removeEventListener("message", eventListener);
+    eventListener = (message) => {
+        console.log("queueing message: "+ JSON.stringify(message));
+        pendingMessages.push(message);
+    };
+    console.log("adding queuing listener");
+    addEventListener("message", eventListener);
+};
+console.log("adding initial listener");
path: engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
user: login: kevmoo
body: We should avoid noise here except in exceptional cases – or for time-bound debugging on our side.
created_at: 2025-03-08T00:16:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,24 +6,67 @@ import { createWasmInstantiator } from "./instantiate_wasm.js";
 import { resolveUrlWithSegments } from "./utils.js";
 
 export const loadSkwasm = async (deps, config, browserEnvironment, baseUrl) => {
-  const fileStem = (browserEnvironment.crossOriginIsolated && !config.forceSingleThreadedSkwasm) ? "skwasm" : "skwasm_st";
-  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, `${fileStem}.js`)
+  const rawSkwasmUrl = resolveUrlWithSegments(baseUrl, 'skwasm.js')
   let skwasmUrl = rawSkwasmUrl;
   if (deps.flutterTT.policy) {
     skwasmUrl = deps.flutterTT.policy.createScriptURL(skwasmUrl);
   }
-  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, `${fileStem}.wasm`));
+  const wasmInstantiator = createWasmInstantiator(resolveUrlWithSegments(baseUrl, 'skwasm.wasm'));
   const skwasm = await import(skwasmUrl);
   return await skwasm.default({
+    skwasmSingleThreaded: !browserEnvironment.crossOriginIsolated || config.forceSingleThreadedSkwasm,
     instantiateWasm: wasmInstantiator,
-    // When hosted via a CDN or some other url that is not the same
-    // origin as the main script of the page, we will fail to create
-    // a web worker with the bootstrapping script. This workaround will
-    // make sure that the worker JS can be loaded regardless of where
-    // it is hosted.
-    mainScriptUrlOrBlob: new Blob(
-      [`import '${skwasmUrl}'`],
-      { 'type': 'application/javascript' },
-    ),
+    locateFile: (filename, scriptDirectory) => {
+      // The wasm workers API has a separate .ww.js file that bootstraps the
+      // web worker. However, it turns out this worker bootstrapper doesn't
+      // actually work with ES6 modules, which we have enabled. So we instead
+      // pass our own bootstrapper that loads skwasm.js as an ES6 module, and
+      // queues/flushes pending messages that were received during the
+      // asynchronous load.
+      if (filename.endsWith('.ww.js')) {
+        const url = resolveUrlWithSegments(baseUrl, filename);
+        return URL.createObjectURL(new Blob(
+          [`
+"use strict";
path: engine/src/flutter/lib/web_ui/flutter_js/src/skwasm_loader.js
user: login: eyebrowsoffire
body: Yeah, I took the original file generated by emscripten and modified it until I got it working properly, and then forgot to go back and clean it up (remove debug print statements, use consistent spacing and `const` and stuff). So I need to go back and clean this up.
created_at: 2025-03-08T01:32:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +15,23 @@
 
 using namespace Skwasm;
 
+Surface::Surface() {
+  if (skwasm_isSingleThreaded()) {
+    skwasm_connectThread(0);
+  } else {
+    assert(emscripten_is_main_browser_thread());
+
+    _thread = emscripten_malloc_wasm_worker(65536);
+    emscripten_wasm_worker_post_function_v(_thread, []() {
path: engine/src/flutter/lib/web_ui/skwasm/surface.cpp
user: login: eyebrowsoffire
body: Yeah I can break down how this works. So this C++ lambda is not actually a closure, since it doesn't actually close over any variables (there are no captures). As such, a stateless lambda with no closures is special-cased in C++, and it decays to a normal vanilla C function pointer. So if this lambda did capture any variables, this wouldn't work at all, the lambda wouldn't be able to decay to a C function pointer. But with no captures, you can essentially think of the compiler just treating it as a scoped static C function.

The `emscripten_wasm_worker_post_function_v` takes a pointer to a C function as an argument. Whenever the address of a function is taken, emscripten/llvm know to add that function to the global function table, and the pointer to that C function is essentially just an index into that global function table. The emscripten wasm workers runtime has JS support code that basically takes this function index and ships it across to the worker via a `postMessage`. When the worker processes this message, it looks up the function in its own global function table by index, and the global function tables should be identical between the instance on the main thread and the instance on the worker.
created_at: 2025-03-08T01:39:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: flutter-dashboard[bot]
created_at: 2025-03-06T23:05:13Z
body: It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption, contact "@test-exemption-reviewer" in the #hackers channel in [Discord](https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md) (don't just cc them here, they won't see it!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, **is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?**

__Reviewers__: Read the [Tree Hygiene page](https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#how-to-review-code) and make sure this patch meets those guidelines before LGTMing. The test exemption team is a small volunteer group, so _all_ reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.
author_association: NONE
user: login: eyebrowsoffire
created_at: 2025-03-07T21:44:39Z
body: This change doesn't require additional unit tests, because there is no change in functionality. The existing unit tests should be sufficient to confirm that we didn't break any existing functionality with this change.
author_association: CONTRIBUTOR
user: login: auto-submit[bot]
created_at: 2025-03-17T22:03:19Z
body: Time to revert pull request flutter/flutter/164748 has elapsed.
          You need to open the revert manually and process as a regular pull request.
author_association: CONTRIBUTOR