From 68e0ff758e9ce1925c9bd4b15425fe19ff4ca549 Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Thu, 4 Dec 2025 17:09:03 +0100 Subject: [PATCH 1/8] Complete rethink of the test --- test/webaudio/audioworklet_emscripten_locks.c | 284 ++++++++++-------- 1 file changed, 154 insertions(+), 130 deletions(-) diff --git a/test/webaudio/audioworklet_emscripten_locks.c b/test/webaudio/audioworklet_emscripten_locks.c index 20471eced5f63..90b2684d73058 100644 --- a/test/webaudio/audioworklet_emscripten_locks.c +++ b/test/webaudio/audioworklet_emscripten_locks.c @@ -1,4 +1,3 @@ -#include #include #include #include @@ -7,180 +6,205 @@ // // - _emscripten_thread_supports_atomics_wait() // - emscripten_lock_init() -// - emscripten_lock_try_acquire() // - emscripten_lock_busyspin_wait_acquire() -// - emscripten_lock_busyspin_waitinf_acquire() // - emscripten_lock_release() -// - emscripten_get_now() +// - emscripten_get_now() in AW + +// Build with emcc -sAUDIO_WORKLET -sWASM_WORKERS -pthread -O1 -g -o index.html audioworklet_emscripten_locks.c + +// Values -1.5373, 77.2259, -251.4728 +// Values -0.9080, -42.4902, -250.6685 + +// Marks a function to be kept in the Module and exposed to script (instead of adding to EXPORTED_FUNCTIONS) +#ifndef KEEP_IN_MODULE +#define KEEP_IN_MODULE __attribute__((used, visibility("default"))) +#endif + +// This needs to be big enough for a stereo output (1024 with a 128 frame) + working stack +#define AUDIO_STACK_SIZE 2048 // Internal, found in 'system/lib/pthread/threading_internal.h' (and requires building with -pthread) int _emscripten_thread_supports_atomics_wait(void); typedef enum { - // No wait support in audio worklets - TEST_HAS_WAIT, - // Acquired in main, fail in process - TEST_TRY_ACQUIRE, - // Keep acquired so time-out - TEST_WAIT_ACQUIRE_FAIL, - // Release in main, succeed in process - TEST_WAIT_ACQUIRE, - // Release in process after above - TEST_RELEASE, - // Released in process above, spin in main - TEST_WAIT_INFINTE_1, - // Release in process to stop spinning in main - TEST_WAIT_INFINTE_2, - // Call emscripten_get_now() in process - TEST_GET_NOW, + // The test hasn't yet started + TEST_NOT_STARTED, + // Worklet ready and running the test + TEST_RUNNING, + // Main thread is finished, wait on worklet + TEST_DONE_MAIN, // Test finished TEST_DONE } Test; +// Global audio context +EMSCRIPTEN_WEBAUDIO_T context; // Lock used in all the tests emscripten_lock_t testLock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; // Which test is running (sometimes in the worklet, sometimes in the main thread) -_Atomic Test whichTest = TEST_HAS_WAIT; +_Atomic Test whichTest = TEST_NOT_STARTED; // Time at which the test starts taken in main() double startTime = 0; -bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) { - assert(emscripten_current_thread_is_audio_worklet()); +// Counter for main, accessed only by main +int howManyMain = 0; +// Counter for the audio worklet, accessed only by the AW +int howManyProc = 0; + +// Our dummy container +typedef struct Dummy { + uint32_t val0; + uint32_t val1; + uint32_t val2; +} Dummy; + +// Start values +void initDummy(Dummy* dummy) { + dummy->val0 = 4; + dummy->val1 = 1; + dummy->val2 = 2; +} - // Produce at few empty frames of audio before we start trying to interact - // with the with main thread. - // On chrome at least it appears the main thread completely blocks until - // a few frames have been produced. This means it may not be safe to interact - // with the main thread during initial frames? - // In my experiments it seems like 5 was the magic number that I needed to - // produce before the main thread could continue to run. - // See https://github.com/emscripten-core/emscripten/issues/24213 - static int count = 0; - if (count++ < 5) return true; - - int result = 0; - switch (whichTest) { - case TEST_HAS_WAIT: - // Should not have wait support here - result = _emscripten_thread_supports_atomics_wait(); - emscripten_outf("TEST_HAS_WAIT: %d (expect: 0)", result); - assert(!result); - whichTest = TEST_TRY_ACQUIRE; - break; - case TEST_TRY_ACQUIRE: - // Was locked after init, should fail to acquire - result = emscripten_lock_try_acquire(&testLock); - emscripten_outf("TEST_TRY_ACQUIRE: %d (expect: 0)", result); - assert(!result); - whichTest = TEST_WAIT_ACQUIRE_FAIL; - break; - case TEST_WAIT_ACQUIRE_FAIL: - // Still locked so we fail to acquire - result = emscripten_lock_busyspin_wait_acquire(&testLock, 100); - emscripten_outf("TEST_WAIT_ACQUIRE_FAIL: %d (expect: 0)", result); - assert(!result); - whichTest = TEST_WAIT_ACQUIRE; - case TEST_WAIT_ACQUIRE: - // Will get unlocked in main thread, so should quickly acquire - result = emscripten_lock_busyspin_wait_acquire(&testLock, 10000); - emscripten_outf("TEST_WAIT_ACQUIRE: %d (expect: 1)", result); - assert(result); - whichTest = TEST_RELEASE; - break; - case TEST_RELEASE: - // Unlock, check the result +void printDummy(Dummy* dummy) { + emscripten_outf("Values: %u, %u, %u", dummy->val0, dummy->val1, dummy->val2); +} + +// Run a simple calculation that will only be stable *if* all values are atomically updated +void runCalcs(Dummy* dummy, int num) { + for (int n = 0; n < num; n++) { + int have = emscripten_lock_busyspin_wait_acquire(&testLock, 100); + assert(have); + dummy->val0 += dummy->val1 * dummy->val2; + dummy->val1 += dummy->val2 * dummy->val0; + dummy->val2 += dummy->val0 * dummy->val1; + dummy->val0 /= 4; + dummy->val1 /= 3; + dummy->val2 /= 2; emscripten_lock_release(&testLock); - result = emscripten_lock_try_acquire(&testLock); - emscripten_outf("TEST_RELEASE: %d (expect: 1)", result); - assert(result); - whichTest = TEST_WAIT_INFINTE_1; - break; - case TEST_WAIT_INFINTE_1: - // Still locked when we enter here but move on in the main thread + } +} + +void stopping() { + emscripten_out("Expect: 949807601, 1303780836, 243502614"); + emscripten_out("Ending test"); + emscripten_destroy_audio_context(context); + emscripten_force_exit(0); +} + +// AW callback +bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, AudioSampleFrame* outputs, int numParams, const AudioParamFrame* params, void* data) { + assert(emscripten_current_thread_is_audio_worklet()); + switch (whichTest) { + case TEST_NOT_STARTED: + whichTest = TEST_RUNNING; break; - case TEST_WAIT_INFINTE_2: - emscripten_lock_release(&testLock); - whichTest = TEST_GET_NOW; + case TEST_RUNNING: + case TEST_DONE_MAIN: + if (howManyProc-- > 0) { + runCalcs((Dummy*) data, 250); + } else { + if (whichTest == TEST_DONE_MAIN) { + // Both loops are finished + whichTest = TEST_DONE; + } + } break; - case TEST_GET_NOW: - result = (int) (emscripten_get_now() - startTime); - emscripten_outf("TEST_GET_NOW: %d (expect: > 0)", result); - assert(result > 0); - whichTest = TEST_DONE; case TEST_DONE: + emscripten_outf("Took %dms (expect: > 0)", (int) (emscripten_get_now() - startTime)); return false; - default: - break; } return true; } -EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext), { - let startButton = document.createElement('button'); - startButton.innerHTML = 'Start playback'; - document.body.appendChild(startButton); - - audioContext = emscriptenGetAudioObject(audioContext); - startButton.onclick = () => { - audioContext.resume(); - }; -}); - -bool MainLoop(double time, void* data) { +// Main thread callback +bool mainLoop(double time, void* data) { assert(!emscripten_current_thread_is_audio_worklet()); - static int didUnlock = false; switch (whichTest) { - case TEST_WAIT_ACQUIRE: - if (!didUnlock) { - emscripten_out("main thread releasing lock"); - // Release here to acquire in process - emscripten_lock_release(&testLock); - didUnlock = true; + case TEST_NOT_STARTED: + break; + case TEST_RUNNING: + if (howManyMain-- > 0) { + runCalcs((Dummy*) data, 1000); + } else { + // Done here, so signal to process() + whichTest = TEST_DONE_MAIN; } break; - case TEST_WAIT_INFINTE_1: - // Spin here until released in process (but don't change test until we know this case ran) - whichTest = TEST_WAIT_INFINTE_2; - emscripten_lock_busyspin_waitinf_acquire(&testLock); - emscripten_out("TEST_WAIT_INFINTE (from main)"); + case TEST_DONE_MAIN: + // Wait for process() to finish break; case TEST_DONE: - // Finished, exit from the main thread - emscripten_out("Test success"); - emscripten_force_exit(0); + printDummy((Dummy*) data); + // 32-bit maths with locks *should* result in these: + assert(((Dummy*) data)->val0 == 949807601 + && ((Dummy*) data)->val1 == 1303780836 + && ((Dummy*) data)->val2 == 243502614); + stopping(); return false; - default: - break; } return true; } -void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) { - int outputChannelCounts[1] = { 1 }; - EmscriptenAudioWorkletNodeCreateOptions options = { .numberOfInputs = 0, .numberOfOutputs = 1, .outputChannelCounts = outputChannelCounts }; - EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "noise-generator", &options, &ProcessAudio, NULL); - emscripten_audio_node_connect(wasmAudioWorklet, audioContext, 0, 0); - InitHtmlUi(audioContext); +KEEP_IN_MODULE void startTest() { + startTime = emscripten_get_now(); + if (emscripten_audio_context_state(context) != AUDIO_CONTEXT_STATE_RUNNING) { + emscripten_resume_audio_context_sync(context); + } + howManyMain = 200; + howManyProc = 200; } -void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) { - WebAudioWorkletProcessorCreateOptions opts = { .name = "noise-generator" }; - emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, NULL); +// HTML button to manually run the test +EM_JS(void, addButton, (), { + var button = document.createElement("button"); + button.appendChild(document.createTextNode("Start Test")); + document.body.appendChild(button); + document.onclick = () => { + if (globalThis._startTest) { + _startTest(); + } + }; +}); + +// Audio processor created, now register the audio callback +void processorCreated(EMSCRIPTEN_WEBAUDIO_T ctx, bool success, void* data) { + assert(success && "Audio worklet failed in processorCreated()"); + emscripten_out("Audio worklet processor created"); + // Single stereo output + int outputChannelCounts[1] = { 1 }; + EmscriptenAudioWorkletNodeCreateOptions opts = { + .numberOfOutputs = 1, + .outputChannelCounts = outputChannelCounts + }; + EMSCRIPTEN_AUDIO_WORKLET_NODE_T worklet = emscripten_create_wasm_audio_worklet_node(ctx, "locks-test", &opts, &process, data); + emscripten_audio_node_connect(worklet, ctx, 0, 0); } -uint8_t wasmAudioWorkletStack[2048]; +// Worklet thread inited, now create the audio processor +void initialised(EMSCRIPTEN_WEBAUDIO_T ctx, bool success, void* data) { + assert(success && "Audio worklet failed in initialised()"); + emscripten_out("Audio worklet initialised"); + WebAudioWorkletProcessorCreateOptions opts = { + .name = "locks-test" + }; + emscripten_create_wasm_audio_worklet_processor_async(ctx, &opts, &processorCreated, data); +} int main() { - // Main thread init and acquire (work passes to the processor) emscripten_lock_init(&testLock); - int hasLock = emscripten_lock_busyspin_wait_acquire(&testLock, 0); - assert(hasLock); - - startTime = emscripten_get_now(); - - emscripten_set_timeout_loop(MainLoop, 10, NULL); - EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(NULL); - emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, NULL); + Dummy* dummy = (Dummy*) malloc(sizeof(Dummy)); + initDummy(dummy); + + char* const workletStack = memalign(16, AUDIO_STACK_SIZE); + assert(workletStack); + // Audio processor callback setup + context = emscripten_create_audio_context(NULL); + assert(context); + emscripten_start_wasm_audio_worklet_thread_async(context, workletStack, AUDIO_STACK_SIZE, initialised, dummy); + + emscripten_set_timeout_loop(mainLoop, 10, dummy); + addButton(); + startTest(); // <-- May need a manual click to start emscripten_exit_with_live_runtime(); } From 6b7e0ed3637e2c9eb5967e3fd08dd562610c047d Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Thu, 4 Dec 2025 17:12:29 +0100 Subject: [PATCH 2/8] Minor tidy --- test/webaudio/audioworklet_emscripten_locks.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/webaudio/audioworklet_emscripten_locks.c b/test/webaudio/audioworklet_emscripten_locks.c index 90b2684d73058..f9787e7fe526a 100644 --- a/test/webaudio/audioworklet_emscripten_locks.c +++ b/test/webaudio/audioworklet_emscripten_locks.c @@ -10,11 +10,6 @@ // - emscripten_lock_release() // - emscripten_get_now() in AW -// Build with emcc -sAUDIO_WORKLET -sWASM_WORKERS -pthread -O1 -g -o index.html audioworklet_emscripten_locks.c - -// Values -1.5373, 77.2259, -251.4728 -// Values -0.9080, -42.4902, -250.6685 - // Marks a function to be kept in the Module and exposed to script (instead of adding to EXPORTED_FUNCTIONS) #ifndef KEEP_IN_MODULE #define KEEP_IN_MODULE __attribute__((used, visibility("default"))) From e6c8f52726fbc016cf4427a65ed10d8fffe3b185 Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Thu, 4 Dec 2025 17:17:16 +0100 Subject: [PATCH 3/8] Minor tidy --- test/webaudio/audioworklet_emscripten_locks.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webaudio/audioworklet_emscripten_locks.c b/test/webaudio/audioworklet_emscripten_locks.c index f9787e7fe526a..5aeb4b1d0cecb 100644 --- a/test/webaudio/audioworklet_emscripten_locks.c +++ b/test/webaudio/audioworklet_emscripten_locks.c @@ -47,7 +47,7 @@ int howManyMain = 0; int howManyProc = 0; // Our dummy container -typedef struct Dummy { +typedef struct { uint32_t val0; uint32_t val1; uint32_t val2; From 955342a002d30d0e213addbc59ca8b531fe160d8 Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Thu, 4 Dec 2025 18:31:31 +0100 Subject: [PATCH 4/8] Should be flake-free --- test/test_browser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_browser.py b/test/test_browser.py index 10271bbfe7f0e..d60fa9a8fc7fa 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5472,7 +5472,6 @@ def test_audio_worklet_params_mixing(self, args): @requires_sound_hardware @requires_shared_array_buffer @also_with_minimal_runtime - @flaky('https://github.com/emscripten-core/emscripten/issues/25245') def test_audio_worklet_emscripten_locks(self): self.btest_exit('webaudio/audioworklet_emscripten_locks.c', cflags=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread']) From 7ab2ad658188d5ab84f1eab4650666c18f66c6db Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Thu, 4 Dec 2025 23:32:12 +0100 Subject: [PATCH 5/8] Lowered lock wait time --- test/webaudio/audioworklet_emscripten_locks.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webaudio/audioworklet_emscripten_locks.c b/test/webaudio/audioworklet_emscripten_locks.c index 5aeb4b1d0cecb..21b97a8f36e36 100644 --- a/test/webaudio/audioworklet_emscripten_locks.c +++ b/test/webaudio/audioworklet_emscripten_locks.c @@ -67,7 +67,7 @@ void printDummy(Dummy* dummy) { // Run a simple calculation that will only be stable *if* all values are atomically updated void runCalcs(Dummy* dummy, int num) { for (int n = 0; n < num; n++) { - int have = emscripten_lock_busyspin_wait_acquire(&testLock, 100); + int have = emscripten_lock_busyspin_wait_acquire(&testLock, 10); assert(have); dummy->val0 += dummy->val1 * dummy->val2; dummy->val1 += dummy->val2 * dummy->val0; From 154ffdefbde67375342df04354a9ef50a236e56f Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Fri, 5 Dec 2025 07:52:06 +0100 Subject: [PATCH 6/8] Something to re-run blocked CI --- test/webaudio/audioworklet_emscripten_locks.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webaudio/audioworklet_emscripten_locks.c b/test/webaudio/audioworklet_emscripten_locks.c index 21b97a8f36e36..a3bf9638bd12e 100644 --- a/test/webaudio/audioworklet_emscripten_locks.c +++ b/test/webaudio/audioworklet_emscripten_locks.c @@ -165,7 +165,7 @@ EM_JS(void, addButton, (), { void processorCreated(EMSCRIPTEN_WEBAUDIO_T ctx, bool success, void* data) { assert(success && "Audio worklet failed in processorCreated()"); emscripten_out("Audio worklet processor created"); - // Single stereo output + // Single mono output int outputChannelCounts[1] = { 1 }; EmscriptenAudioWorkletNodeCreateOptions opts = { .numberOfOutputs = 1, From 501eb5152479d6dea29addd2e3a684491c9334e7 Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Fri, 5 Dec 2025 12:38:21 +0100 Subject: [PATCH 7/8] Improved symmetry The test function is called approx. 200'000x from each thread. --- test/webaudio/audioworklet_emscripten_locks.c | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/test/webaudio/audioworklet_emscripten_locks.c b/test/webaudio/audioworklet_emscripten_locks.c index a3bf9638bd12e..36d56ecb413a7 100644 --- a/test/webaudio/audioworklet_emscripten_locks.c +++ b/test/webaudio/audioworklet_emscripten_locks.c @@ -18,6 +18,9 @@ // This needs to be big enough for a stereo output (1024 with a 128 frame) + working stack #define AUDIO_STACK_SIZE 2048 +// Define DISABLE_LOCKS to run the test without locking, which should statistically always fail +//#define DISABLE_LOCKS + // Internal, found in 'system/lib/pthread/threading_internal.h' (and requires building with -pthread) int _emscripten_thread_supports_atomics_wait(void); @@ -65,10 +68,13 @@ void printDummy(Dummy* dummy) { } // Run a simple calculation that will only be stable *if* all values are atomically updated +// (Currently called approx. 200'000x from each thread) void runCalcs(Dummy* dummy, int num) { for (int n = 0; n < num; n++) { +#ifndef DISABLE_LOCKS int have = emscripten_lock_busyspin_wait_acquire(&testLock, 10); assert(have); +#endif dummy->val0 += dummy->val1 * dummy->val2; dummy->val1 += dummy->val2 * dummy->val0; dummy->val2 += dummy->val0 * dummy->val1; @@ -80,7 +86,7 @@ void runCalcs(Dummy* dummy, int num) { } void stopping() { - emscripten_out("Expect: 949807601, 1303780836, 243502614"); + emscripten_out("Expect: 811100370, 759556424, 723197652"); emscripten_out("Ending test"); emscripten_destroy_audio_context(context); emscripten_force_exit(0); @@ -96,16 +102,16 @@ bool process(int numInputs, const AudioSampleFrame* inputs, int numOutputs, Audi case TEST_RUNNING: case TEST_DONE_MAIN: if (howManyProc-- > 0) { - runCalcs((Dummy*) data, 250); + runCalcs((Dummy*) data, 267); // <-- process gets called 3.75x more than main } else { if (whichTest == TEST_DONE_MAIN) { + emscripten_outf("Worklet done after %dms (expect: > 2s)", (int) (emscripten_get_now() - startTime)); // Both loops are finished whichTest = TEST_DONE; } } break; case TEST_DONE: - emscripten_outf("Took %dms (expect: > 0)", (int) (emscripten_get_now() - startTime)); return false; } return true; @@ -121,6 +127,7 @@ bool mainLoop(double time, void* data) { if (howManyMain-- > 0) { runCalcs((Dummy*) data, 1000); } else { + emscripten_outf("Main thread done after %dms (expect: > 2s)", (int) (emscripten_get_now() - startTime)); // Done here, so signal to process() whichTest = TEST_DONE_MAIN; } @@ -131,9 +138,9 @@ bool mainLoop(double time, void* data) { case TEST_DONE: printDummy((Dummy*) data); // 32-bit maths with locks *should* result in these: - assert(((Dummy*) data)->val0 == 949807601 - && ((Dummy*) data)->val1 == 1303780836 - && ((Dummy*) data)->val2 == 243502614); + assert(((Dummy*) data)->val0 == 811100370 + && ((Dummy*) data)->val1 == 759556424 + && ((Dummy*) data)->val2 == 723197652); stopping(); return false; } @@ -146,7 +153,7 @@ KEEP_IN_MODULE void startTest() { emscripten_resume_audio_context_sync(context); } howManyMain = 200; - howManyProc = 200; + howManyProc = 750; // <-- process gets called 3.75x more than main } // HTML button to manually run the test From e00ba55aeba8ad0416db7500c8152f50bc417cad Mon Sep 17 00:00:00 2001 From: Carl Woffenden Date: Fri, 5 Dec 2025 12:45:56 +0100 Subject: [PATCH 8/8] (Failure due to Firefox not downloading on CI) --- test/webaudio/audioworklet_emscripten_locks.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webaudio/audioworklet_emscripten_locks.c b/test/webaudio/audioworklet_emscripten_locks.c index 36d56ecb413a7..f9459c3aa7844 100644 --- a/test/webaudio/audioworklet_emscripten_locks.c +++ b/test/webaudio/audioworklet_emscripten_locks.c @@ -68,7 +68,7 @@ void printDummy(Dummy* dummy) { } // Run a simple calculation that will only be stable *if* all values are atomically updated -// (Currently called approx. 200'000x from each thread) +// (Currently approx. 200'000x from each thread) void runCalcs(Dummy* dummy, int num) { for (int n = 0; n < num; n++) { #ifndef DISABLE_LOCKS