Skip to content

[AUDIO_WORKLET] Enable AW spinlocks, verify them in the browser tests #23729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion system/lib/pthread/emscripten_thread_state.S
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ emscripten_is_main_browser_thread:
global.get is_main_thread
end_function

# Semantically the same as testing "!ENVIRONMENT_IS_WEB" in JS
# Semantically the same as testing "!ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_AUDIO_WORKLET" in JS
.globl _emscripten_thread_supports_atomics_wait
_emscripten_thread_supports_atomics_wait:
.functype _emscripten_thread_supports_atomics_wait () -> (i32)
Expand Down
6 changes: 4 additions & 2 deletions system/lib/wasm_worker/library_wasm_worker.c
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,17 @@ void emscripten_lock_waitinf_acquire(emscripten_lock_t *lock) {
}

bool emscripten_lock_busyspin_wait_acquire(emscripten_lock_t *lock, double maxWaitMilliseconds) {
// TODO: we changed the performance_now calls to get_now, which can be applied
// to the remaining code (since all calls defer to the best internal option).
emscripten_lock_t val = emscripten_atomic_cas_u32((void*)lock, 0, 1);
if (!val) return true;

double t = emscripten_performance_now();
double t = emscripten_get_now();
double waitEnd = t + maxWaitMilliseconds;
while (t < waitEnd) {
val = emscripten_atomic_cas_u32((void*)lock, 0, 1);
if (!val) return true;
t = emscripten_performance_now();
t = emscripten_get_now();
}
return false;
}
Expand Down
9 changes: 5 additions & 4 deletions test/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5516,12 +5516,13 @@ def test_audio_worklet_params_mixing(self, args):
shutil.copy(test_file('webaudio/audio_files/emscripten-bass.mp3'), 'audio_files/')
self.btest_exit('webaudio/audioworklet_params_mixing.c', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-DTEST_AND_EXIT'] + args)

# Tests AudioWorklet with emscripten_futex_wake().
# Tests AudioWorklet with emscripten_lock_busyspin_wait_acquire() and friends
@no_wasm64('https://github.com/emscripten-core/emscripten/pull/23508')
@no_2gb('https://github.com/emscripten-core/emscripten/pull/23508')
@requires_sound_hardware
@also_with_minimal_runtime
@disabled('https://github.com/emscripten-core/emscripten/issues/22962')
def test_audio_worklet_emscripten_futex_wake(self):
self.btest('webaudio/audioworklet_emscripten_futex_wake.cpp', expected='0', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread', '-sPTHREAD_POOL_SIZE=2'])
def test_audio_worklet_emscripten_locks(self):
self.btest_exit('webaudio/audioworklet_emscripten_futex_wake.cpp', args=['-sAUDIO_WORKLET', '-sWASM_WORKERS', '-pthread'])

def test_error_reporting(self):
# Test catching/reporting Error objects
Expand Down
162 changes: 128 additions & 34 deletions test/webaudio/audioworklet_emscripten_futex_wake.cpp
Original file line number Diff line number Diff line change
@@ -1,32 +1,105 @@
#include <emscripten/webaudio.h>
#include <emscripten/threading.h>
#include <stdio.h>
#include <stdlib.h>
#include <emscripten/wasm_worker.h>
#include <emscripten/webaudio.h>
#include <assert.h>

// Tests that
// - _emscripten_thread_supports_atomics_wait() returns true in a Wasm Audio Worklet.
// - emscripten_futex_wake() does not crash in a Wasm Audio Worklet.
// - emscripten_futex_wait() does not crash in a Wasm Audio Worklet.
// - emscripten_get_now() does not crash in a Wasm Audio Worklet.

int futexLocation = 0;
int testSuccess = 0;
// Tests that these audio worklet compatible functions work, details in comments below:
//
// - _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()

// Internal, found in 'system/lib/pthread/threading_internal.h'
extern "C" int _emscripten_thread_supports_atomics_wait();
// Internal, found in 'system/lib/pthread/threading_internal.h' (and requires building with -pthread)
extern "C" int _emscripten_thread_supports_atomics_wait(void);

bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) {
int supportsAtomicWait = _emscripten_thread_supports_atomics_wait();
printf("supportsAtomicWait: %d\n", supportsAtomicWait);
assert(!supportsAtomicWait);
emscripten_futex_wake(&futexLocation, 1);
printf("%f\n", emscripten_get_now());
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,
// Test finished
TEST_DONE
} Test;

emscripten_futex_wait(&futexLocation, 1, /*maxWaitMs=*/2);
testSuccess = 1;
// 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;
// Time at which the test starts taken in main()
double startTime = 0;

return false;
bool ProcessAudio(int numInputs, const AudioSampleFrame *inputs, int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData) {
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, 100);
emscripten_outf("TEST_WAIT_ACQUIRE: %d (expect: 1)", result);
assert(result);
whichTest = TEST_RELEASE;
break;
case TEST_RELEASE:
// Unlock, check the result
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
break;
case TEST_WAIT_INFINTE_2:
emscripten_lock_release(&testLock);
whichTest = TEST_GET_NOW;
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:
return false;
default:
break;
}
return true;
}

EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext), {
Expand All @@ -40,34 +113,55 @@ EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext), {
};
});

bool PollTestSuccess(double, void *) {
if (testSuccess) {
printf("Test success!\n");
#ifdef REPORT_RESULT
REPORT_RESULT(0);
#endif
bool MainLoop(double time, void* data) {
switch (whichTest) {
case TEST_WAIT_ACQUIRE:
// Release here to acquire in process
emscripten_lock_release(&testLock);
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)");
break;
case TEST_DONE:
// Finished, exit from the main thread
emscripten_out("Test success");
emscripten_force_exit(0);
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, 0);
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);
}

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, 0);
emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, NULL);
}

uint8_t wasmAudioWorkletStack[4096];
uint8_t wasmAudioWorkletStack[2048];

int main() {
emscripten_set_timeout_loop(PollTestSuccess, 10, 0);
EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(0);
emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0);
// 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);

emscripten_exit_with_live_runtime();
}