Repository navigation
Description
Summary
In #190048 (fixing #190039) and #191014, skwasm enabled SK_ENABLE_THREADLOCAL_STRIKECACHE because multithreaded skwasm and wimp are compiled with -sWASM_WORKERS=1 (engine/src/build/config/compiler/BUILD.gn:312) without -pthread, which links single-threaded Emscripten system libraries where POSIX synchronization primitives (sem_wait, pthread_cond_wait) are no-op stubs.
Making SkStrikeCache thread-local did not eliminate shared mutable state between main-thread $paragraph_layout and DedicatedWorker _surface_renderPicturesOnWorker:
- Both threads share the same
SkFontMgr,FlutterFontCollection, andSkTypeface_FreeType(FT_Face/FT_Library) instances in sharedWebAssembly.Memory(engine/src/flutter/skwasm/fonts.cc:38-42). - Whenever the main thread's thread-local
SkStrike(during$paragraph_layout) and the worker thread's thread-localSkStrike(during_surface_renderPicturesOnWorker) miss at the same time on the same typeface (for example, when animating layout orFittedBoxscales), both threads concurrently enterSkScalerContext_FreeType::generateMetrics/generateImage->FT_Load_Glyph->TT_Load_Glyphon the same non-thread-safe FreeTypeFT_Face. - In Skia (
SkFontHost_FreeType.cpp), concurrentSkScalerContext_FreeTypeinstances across threads rely onf_t_mutex()(SkMutex) andfFaceRec->fFTMutex(SkMutex) — backed bySkSemaphore(third_party/skia/src/base/SkSemaphore.cpp) — to serialize access toFT_LibraryandFT_Face. - Because
SkSemaphore::osWait()(sem_wait) compiles to a no-op stub (0blocking or spinning instructions) in bothskwasm.wasm(wasm-function[268]) andwimp.wasm(wasm-function[302]),f_t_mutex().acquire()returns immediately when contended (fCount.fetch_sub(1) <= 0), allowing both threads to mutateFT_Facesimultaneously.
Steps to Reproduce
- Build
kevmoo/flutter-wasm-compareat commit5c4e80ewithflutter build web --wasm(3.49.0-1.0.pre-171, engine1a3d11d5ab1). - Serve
build/webwithCross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpsoskwasm_isMultiThreaded() == true. - Navigate to
?workload=grid&nodes=100&renderer=skwasm(orrenderer=wimp, ornodes=1000), which animates 100GridCellcards containingFittedBox+Textparagraphs every frame while theDedicatedWorkerrasterizes the previous frame.
Observed Behavior
Across repeated runs on both macOS (Apple M4 Pro, Chrome 146) and Linux (x86_64, Headless Chrome 154), multi-threaded grid@100 and grid@1000 intermittently fail in one of two ways (0 failures in single-threaded skwasm or wimp across 100+ runs):
- Uncaught
RuntimeError: memory access out of boundsin$paragraph_layout->FT_Load_Glyph->TT_Load_Simple_Glyph:
Instrumenting workerpostMessage(cbId) andRuntime.exceptionThrownvia CDP shows that while theDedicatedWorkeris mid-flight executing_surface_renderPicturesOnWorker(cbId = 23, dispatched att = 1721.6 ms, completed att = 1753.1 ms), the main UI thread throws att = 1731.0 ms:
RuntimeError: memory access out of bounds
at skwasm.wasm:wasm-function[2107]:0x63e5b // TT_Load_Simple_Glyph (FreeType ttgload.c)
at skwasm.wasm:wasm-function[10931]:0x2863fd // load_truetype_glyph (FreeType ttgload.c)
at skwasm.wasm:wasm-function[10942]:0x28832c // TT_Load_Glyph (FreeType ttgload.c)
at skwasm.wasm:wasm-function[721]:0x1f3c1 // FT_Load_Glyph (FreeType ftobjs.c)
at skwasm.wasm:wasm-function[11471]:0x2a7ee9 // SkScalerContext_FreeType::generateMetrics (calls f_t_mutex() func 452 -> func 268)
at skwasm.wasm:wasm-function[6734]:0x185fda // SkScalerContext::makeGlyph
at skwasm.wasm:wasm-function[1776]:0x51f44 // SkStrike::digestFor
at skwasm.wasm:wasm-function[2783]:0x8e456 // SkBulkGlyphMetrics::glyph
at skwasm.wasm:wasm-function[6681]:0x182bb3 // SkBulkGlyphMetrics::glyphs
at skwasm.wasm:wasm-function[2246]:0x6b813 // SkStrike::lock / glyph metrics
...
at skwasm.wasm:wasm-function[9227]:0x227493 // skia::textlayout::ParagraphImpl::layout
at $paragraph_layout (skwasm.wasm:0x26ee84) // engine/src/flutter/skwasm/text/paragraph.cc:55
This uncaught trap aborts PipelineOwner.flushLayout() mid-frame and stops all subsequent scene production while requestAnimationFrame continues firing.
- Main-thread infinite busy-spin in
dlmalloc:
If a thread traps while holding Emscripten'sdlmallocspinlock (wasm-function[276]/wasm-function[222], spinning oni32.atomic.rmw.xchg offset=603928) or concurrent FreeTypeFT_Memoryallocations corrupt allocator state, the next main-thread allocation spins indefinitely indlmalloc, hanging the tab.
Disassembling wasm-function[268] in skwasm.wasm (and identical wasm-function[302] in wimp.wasm) confirms that SkSemaphore::wait() returns immediately without waiting when fCount.fetch_sub(1) <= 0:
(func (;268;) (type 2) (param i32)
block ;; label = @1
block ;; label = @2
local.get 0
i32.const 1
i32.atomic.rmw.sub
i32.const 0
i32.gt_s
br_if 0 (;@2;) ;; Uncontended (old fCount > 0): lock acquired, return.
block ;; label = @3
block ;; label = @4
local.get 0
i32.atomic.load8_u offset=4 ;; SkOnce state (0=NotStarted, 1=Claimed, 2=Done)
br_table 0 (;@4;) 1 (;@3;) 2 (;@2;) 1 (;@3;)
;; When SkOnce == 2 (Done), jumps directly to @2 (return) with zero wait instructions.
end
local.get 0
i32.const 0
i32.const 1
i32.atomic.rmw8.cmpxchg_u offset=4
i32.eqz
br_if 2 (;@1;)
end
loop ;; label = @3
local.get 0
i32.atomic.load8_u offset=4
i32.const 2
i32.ne
br_if 0 (;@3;)
end
end
return
end
local.get 0
i32.const 16
call 224 ;; operator new(16) for OSSemaphore (sem_t)
i32.store offset=8
local.get 0
i32.const 2
i32.atomic.store8 offset=4
)Expected Behavior
SkMutex / SkSemaphore (third_party/skia/src/base/SkSemaphore.cpp) must enforce mutual exclusion when compiled with -sWASM_WORKERS=1 (e.g., via Wasm atomics / spin-wait on the main browser thread and emscripten_futex_wait on workers), or SkTypeface_FreeType (FT_Face) must not be shared across threads without synchronization.
Environment / Target
- Target Commit / Version: Flutter
3.49.0-1.0.pre-171(engine1a3d11d5ab1, after [web] Use thread local strike caches in skwasm #190048 and Use officially supported compile time thread local strike cache flag. #191014) - Runtime / Platform: WebAssembly (
skwasmandwimpmulti-threaded under COOP/COEP), Chrome 146 (macOS arm64) and Chrome 154 (Linux x86_64)
Activity
kevmoo commentedon Oct 16, 2026
#194139 (
-Wl,--wrap=sem_*) is required for bothskwasmandwimpmulti-threaded builds—neitherSkTypeface::makeClonenoremscripten_futex_waitcan replace it:Why
-Wl,--wrap=sem_*([skwasm] Implement Wasm worker futex semaphores #194139) is required even if typefaces are cloned:In
SkFontHost_FreeType.cpp,f_t_mutex()is a single process-wideSkMutexprotecting bothgFTLibraryand everySkScalerContext_FreeTypemethod (setupSizeat L1130,generateMetricsat L1221,generateImageat L1400, andgenerateFontMetricsat L1565), whileSkTypeface_FreeType::fC2GCacheMutex(SkTypeface_FreeType.h:87) is anSkSharedMutexbacked bySkSemaphore(SkSharedMutex.cpp:281). Moving glyph rasterization onto the UI thread in WIMP would also pull FreeType outline rasterization back onto the UI frame budget and leaveskwasm's Ganesh worker path (render_context_skia.cc:39-49) unprotected.Why
emscripten_futex_wait/emscripten_futex_wakecannot be used under-sWASM_WORKERS=1:In Emscripten's
tools/system_libs.py(libc.get_files()),-sWASM_WORKERS=1without-pthreadlinkssystem/lib/pthread/library_pthread_stub.cintolibc-ww.a, whereemscripten_futex_waitandemscripten_futex_wake(lines 24–33) are stubbed toreturn 0just likesem_init/sem_wait/sem_post(lines 359–377). Additionally, the WebAssembly Threads spec forbidsmemory.atomic.wait32(Atomics.wait) on the main browser thread ([[CanBlock]] == false) regardless of timeout, so spinning on worker ID0while callingemscripten_atomic_wait_u32on worker threads in [skwasm] Implement Wasm worker futex semaphores #194139 is the exact required behavior (matching Emscripten's ownfutex_wait_main_browser_threadinemscripten_futex_wait.c).Cached
thread_localworker check in [skwasm] Implement Wasm worker futex semaphores #194139 & upstream Skia follow-up:emscripten_wasm_worker_self_id()is a JS FFI import (() => Module['$ww']insrc/library_wasm_worker.js:247); [skwasm] Implement Wasm worker futex semaphores #194139 now cachesstatic thread_local const bool can_block = emscripten_wasm_worker_self_id() != 0;inside__wrap_sem_waitto avoid a Wasm-to-JS call on contended waits. Upstreaming a native#elif defined(__EMSCRIPTEN_WASM_WORKERS__)branch intoSkSemaphore.cppcan follow in Skia soosSignal(int n)can also wakenwaiters in a singleemscripten_atomic_notify(count, n)call.[skwasm] Implement Wasm worker futex semaphores (flutter#194139)
[skwasm] Implement Wasm worker futex semaphores (flutter#194139)
github-actions commentedon Oct 31, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.