diff --git a/src/lib/libcore.js b/src/lib/libcore.js index 5247fbfd06284..6c998e7a65e5f 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -2138,7 +2138,9 @@ addToLibrary({ // exit the current thread, but only if there is one active. // TODO(https://github.com/emscripten-core/emscripten/issues/25076): // Unify this check with the runtimeExited check above - if (_pthread_self()) __emscripten_thread_exit(EXITSTATUS); + // `EXITSTATUS` holds the thread result if the entry point has already + // returned, but is unset if the thread unwound to the event loop. + if (_pthread_self()) __emscripten_thread_exit(EXITSTATUS ?? 0); return; } #endif diff --git a/system/include/emscripten/proxying.h b/system/include/emscripten/proxying.h index 380adcb5cb932..29d2b99d057a0 100644 --- a/system/include/emscripten/proxying.h +++ b/system/include/emscripten/proxying.h @@ -49,6 +49,20 @@ typedef struct em_proxying_ctx em_proxying_ctx; // Signal the end of a task proxied with `emscripten_proxy_sync_with_ctx`. void emscripten_proxy_finish(em_proxying_ctx* ctx); +// Declare that a task proxied with `emscripten_proxy_sync_with_ctx` will no +// longer access `arg`. By default a canceled caller cannot exit while the task +// is running, since `arg` may be on the caller's stack; after this call it may +// exit as soon as it is canceled, even though the task has not finished. +// `emscripten_proxy_finish` implies this release. +void emscripten_proxy_release_arg(em_proxying_ctx* ctx); + +// Attempt to reverse `emscripten_proxy_release_arg`, re-pinning the caller so +// the task may access `arg` again (typically to write results back before +// finishing). Returns false if the caller has already been canceled and its +// `arg` is gone, in which case `arg` must not be accessed. After a successful +// acquire, a canceled caller once again waits for a release or finish. +bool emscripten_proxy_acquire_arg(em_proxying_ctx* ctx); + // Enqueue `func` on the given queue and thread and return immediately. Returns // true if the work was successfully enqueued and the target thread notified or // false otherwise. @@ -60,7 +74,10 @@ bool emscripten_proxy_async(em_proxying_queue* q, // Enqueue `func` on the given queue and thread and wait for it to finish // executing before returning. Returns true if the task was successfully // completed and false otherwise, including if the target thread is canceled or -// exits before the work is completed. +// exits before the work is completed. The wait is a cancellation point: a +// canceled caller exits with PTHREAD_CANCELED once `func` has completed (or +// immediately if `func` has not yet started, in which case it is dropped), +// since `arg` may be on the caller's stack. bool emscripten_proxy_sync(em_proxying_queue* q, pthread_t target_thread, void (*func)(void*), @@ -72,7 +89,11 @@ bool emscripten_proxy_sync(em_proxying_queue* q, // instead store the context pointer and call `emscripten_proxy_finish` at an // arbitrary later time. Returns true if the task was successfully completed and // false otherwise, including if the target thread is canceled or exits before -// the work is completed. +// the work is completed. The wait is a cancellation point: a canceled caller +// exits with PTHREAD_CANCELED once the task is finished (or canceled), or +// earlier if the task has not started or has called +// `emscripten_proxy_release_arg`, since `arg` may be on the caller's stack. If +// the caller is canceled before the task starts, the task is dropped. bool emscripten_proxy_sync_with_ctx(em_proxying_queue* q, pthread_t target_thread, void (*func)(em_proxying_ctx*, void*), diff --git a/system/lib/pthread/proxying.c b/system/lib/pthread/proxying.c index 1342cb2cc924f..9d9784289fa1c 100644 --- a/system/lib/pthread/proxying.c +++ b/system/lib/pthread/proxying.c @@ -8,8 +8,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -190,7 +193,19 @@ bool emscripten_proxy_async(em_proxying_queue* q, enum ctx_kind { SYNC, CALLBACK }; -enum ctx_state { PENDING, DONE, CANCELED }; +// The sync ctx state word tracks ownership with three flags. `arg` is +// caller-owned (typically on its stack) and loaned to the task by +// `emscripten_proxy_acquire_arg` (which is also how the task starts) until +// `emscripten_proxy_release_arg` or completion. While the loan is outstanding +// a canceled caller is pinned in its wait; otherwise it sets CTX_ORPHANED and +// exits immediately, handing ownership of the ctx to the target to recycle at +// completion (and voiding all future loans, including a task not yet started, +// which is dropped). CTX_LOANED, CTX_DONE, and CTX_ORPHANED are mutually +// exclusive. +#define CTX_LOANED 1u // The task may access `arg`; the caller is pinned. +#define CTX_DONE 2u // The call is over; with CTX_OK unless the target died. +#define CTX_OK 4u +#define CTX_ORPHANED 8u // The canceled caller is gone; `arg` with it. struct em_proxying_ctx { // The user-provided function and argument. @@ -199,13 +214,13 @@ struct em_proxying_ctx { enum ctx_kind kind; union { - // Context for synchronous proxying. + // Context for synchronous proxying. Allocated from a per-thread pool so + // that a caller canceled and exiting mid-wait leaves the target a valid + // ctx to finish (or cancel) and recycle later. struct { - // Update `state` and signal the condition variable once the proxied task - // is done or canceled. - enum ctx_state state; - pthread_mutex_t mutex; - pthread_cond_t cond; + // Single-word lifecycle for the sync handshake; the caller futex-waits + // on it directly. See the CTX_* flags above. + _Atomic uint32_t state; } sync; // Context for proxying with callbacks. @@ -229,15 +244,46 @@ struct em_proxying_ctx { static pthread_key_t active_ctxs; static pthread_once_t active_ctxs_once = PTHREAD_ONCE_INIT; +// A per-thread free list of sync ctxs, freed to the heap on thread exit. Sync +// ctxs cannot live on the caller's stack since a canceled caller may exit +// while the target still holds its ctx, and pooling avoids a heap allocation +// per proxied call. +static pthread_key_t ctx_pool; + static void cancel_ctx(void* arg); static void cancel_active_ctxs(void* arg); +static void free_ctx_pool(void* head) { + em_proxying_ctx* ctx = head; + while (ctx) { + em_proxying_ctx* next = ctx->next; + free(ctx); + ctx = next; + } +} + static void init_active_ctxs(void) { int ret = pthread_key_create(&active_ctxs, cancel_active_ctxs); assert(ret == 0); + ret = pthread_key_create(&ctx_pool, free_ctx_pool); + assert(ret == 0); (void)ret; } +static em_proxying_ctx* sync_ctx_alloc(void) { + em_proxying_ctx* ctx = pthread_getspecific(ctx_pool); + if (ctx) { + pthread_setspecific(ctx_pool, ctx->next); + return ctx; + } + return malloc(sizeof(em_proxying_ctx)); +} + +static void sync_ctx_free(em_proxying_ctx* ctx) { + ctx->next = pthread_getspecific(ctx_pool); + pthread_setspecific(ctx_pool, ctx); +} + static void add_active_ctx(em_proxying_ctx* ctx) { assert(ctx != NULL); em_proxying_ctx* head = pthread_getspecific(active_ctxs); @@ -291,18 +337,11 @@ static void cancel_active_ctxs(void* arg) { static void em_proxying_ctx_init_sync(em_proxying_ctx* ctx, void (*func)(em_proxying_ctx*, void*), void* arg) { - pthread_once(&active_ctxs_once, init_active_ctxs); - *ctx = (em_proxying_ctx){ - .func = func, - .arg = arg, - .kind = SYNC, - .sync = - { - .state = PENDING, - .mutex = PTHREAD_MUTEX_INITIALIZER, - .cond = PTHREAD_COND_INITIALIZER, - }, - }; + ctx->func = func; + ctx->arg = arg; + ctx->kind = SYNC; + ctx->next = ctx->prev = NULL; + atomic_store(&ctx->sync.state, 0); } static void em_proxying_ctx_init_callback(em_proxying_ctx* ctx, @@ -327,19 +366,25 @@ static void em_proxying_ctx_init_callback(em_proxying_ctx* ctx, }; } -static void em_proxying_ctx_deinit(em_proxying_ctx* ctx) { - if (ctx->kind == SYNC) { - pthread_mutex_destroy(&ctx->sync.mutex); - pthread_cond_destroy(&ctx->sync.cond); - } - // TODO: We should probably have some kind of refcounting scheme to keep - // `queue` alive for callback ctxs. -} +// TODO: We should probably have some kind of refcounting scheme to keep +// `queue` alive for callback ctxs. +static void free_ctx(void* arg) { free(arg); } -static void free_ctx(void* arg) { - em_proxying_ctx* ctx = arg; - em_proxying_ctx_deinit(ctx); - free(ctx); +// Publish completion (releasing any outstanding loan) and either recycle the +// ctx of a canceled caller or wake the waiting one. After this the target must +// not touch the ctx: waking a possibly recycled address is benign (futex +// waiters recheck), but nothing else would be. +static void sync_ctx_complete(em_proxying_ctx* ctx, uint32_t flags) { + uint32_t s = atomic_load(&ctx->sync.state); + while (!atomic_compare_exchange_weak( + &ctx->sync.state, &s, (s & CTX_ORPHANED) | flags)) { + } + assert(!(s & CTX_DONE)); + if (s & CTX_ORPHANED) { + sync_ctx_free(ctx); + } else { + emscripten_futex_wake(&ctx->sync.state, 1); + } } // Free the callback info on the same thread it was originally allocated on. @@ -350,16 +395,35 @@ static void call_callback_then_free_ctx(void* arg) { free_ctx(ctx); } +void emscripten_proxy_release_arg(em_proxying_ctx* ctx) { + assert(ctx->kind == SYNC); + // No other flag can be set while the loan is outstanding, so a plain store + // cannot lose one. + assert(atomic_load(&ctx->sync.state) == CTX_LOANED); + atomic_store(&ctx->sync.state, 0); + emscripten_futex_wake(&ctx->sync.state, 1); +} + +bool emscripten_proxy_acquire_arg(em_proxying_ctx* ctx) { + assert(ctx->kind == SYNC); + uint32_t s = atomic_load(&ctx->sync.state); + do { + assert(!(s & CTX_DONE)); + if (s & CTX_ORPHANED) { + return false; + } + if (s & CTX_LOANED) { + return true; + } + } while ( + !atomic_compare_exchange_weak(&ctx->sync.state, &s, s | CTX_LOANED)); + return true; +} + void emscripten_proxy_finish(em_proxying_ctx* ctx) { if (ctx->kind == SYNC) { - pthread_mutex_lock(&ctx->sync.mutex); - ctx->sync.state = DONE; remove_active_ctx(ctx); - // Signal must come before unlock to avoid emscripten_proxy_sync_with ctx - // seeing the state as DONE and freeing the ctx before we call unlock. - // See https://github.com/emscripten-core/emscripten/pull/26582 - pthread_cond_signal(&ctx->sync.cond); - pthread_mutex_unlock(&ctx->sync.mutex); + sync_ctx_complete(ctx, CTX_DONE | CTX_OK); } else { // Schedule the callback on the caller thread. If the caller thread has // already died or dies before the callback is executed, then at least make @@ -382,11 +446,7 @@ static void call_cancel_then_free_ctx(void* arg) { static void cancel_ctx(void* arg) { em_proxying_ctx* ctx = arg; if (ctx->kind == SYNC) { - pthread_mutex_lock(&ctx->sync.mutex); - ctx->sync.state = CANCELED; - // Signal must be first, see comment in emscripten_proxy_finish. - pthread_cond_signal(&ctx->sync.cond); - pthread_mutex_unlock(&ctx->sync.mutex); + sync_ctx_complete(ctx, CTX_DONE); } else { if (ctx->cb.cancel == NULL || !do_proxy(ctx->cb.queue, @@ -400,29 +460,73 @@ static void cancel_ctx(void* arg) { // Helper for wrapping the call with ctx as a `void (*)(void*)`. static void call_with_ctx(void* arg) { em_proxying_ctx* ctx = arg; + // A sync task starts by taking the loan of `arg`; if the caller was already + // canceled its `arg` is gone and no one wants the result, so drop the work. + if (ctx->kind == SYNC && !emscripten_proxy_acquire_arg(ctx)) { + sync_ctx_free(ctx); + return; + } add_active_ctx(ctx); ctx->func(ctx, ctx->arg); } +// Handle cancellation of a caller waiting below. Hold the caller's stack +// alive while the loan of `arg` is outstanding, then hand the ctx over to the +// target, or recycle it if the call already completed. Runs after the masked +// cancel has set the cancel state to disabled, so waiting here cannot itself +// be canceled. +static void orphan_sync_ctx(em_proxying_ctx* ctx) { + uint32_t s = atomic_load(&ctx->sync.state); + while (1) { + if (s & CTX_LOANED) { + emscripten_futex_wait(&ctx->sync.state, s, INFINITY); + s = atomic_load(&ctx->sync.state); + } else if (s & CTX_DONE) { + sync_ctx_free(ctx); + return; + } else if (atomic_compare_exchange_weak( + &ctx->sync.state, &s, s | CTX_ORPHANED)) { + return; + } + } +} + bool emscripten_proxy_sync_with_ctx(em_proxying_queue* q, pthread_t target_thread, void (*func)(em_proxying_ctx*, void*), void* arg) { assert(!pthread_equal(target_thread, pthread_self()) && "Cannot synchronously wait for work proxied to the current thread"); - em_proxying_ctx ctx; - em_proxying_ctx_init_sync(&ctx, func, arg); - if (!do_proxy(q, target_thread, (task){call_with_ctx, cancel_ctx, &ctx})) { - em_proxying_ctx_deinit(&ctx); + pthread_once(&active_ctxs_once, init_active_ctxs); + em_proxying_ctx* ctx = sync_ctx_alloc(); + if (!ctx) { return false; } - pthread_mutex_lock(&ctx.sync.mutex); - while (ctx.sync.state == PENDING) { - pthread_cond_wait(&ctx.sync.cond, &ctx.sync.mutex); + em_proxying_ctx_init_sync(ctx, func, arg); + if (!do_proxy(q, target_thread, (task){call_with_ctx, cancel_ctx, ctx})) { + sync_ctx_free(ctx); + return false; + } + // The wait is a cancellation point. Mask cancellation (as in + // pthread_cond_timedwait) so it surfaces as ECANCELED rather than unwinding + // the thread from inside the wait, hand the ctx over to the target, then + // exit. + int cs; + __pthread_setcancelstate(PTHREAD_CANCEL_MASKED, &cs); + if (cs == PTHREAD_CANCEL_DISABLE) { + __pthread_setcancelstate(cs, 0); } - pthread_mutex_unlock(&ctx.sync.mutex); - int ret = ctx.sync.state == DONE; - em_proxying_ctx_deinit(&ctx); + uint32_t s; + while (!((s = atomic_load(&ctx->sync.state)) & CTX_DONE)) { + if (emscripten_futex_wait(&ctx->sync.state, s, INFINITY) == -ECANCELED) { + orphan_sync_ctx(ctx); + __pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, 0); + __pthread_testcancel(); + } + } + __pthread_setcancelstate(cs, 0); + bool ret = s & CTX_OK; + sync_ctx_free(ctx); return ret; } @@ -648,14 +752,40 @@ static void run_js_func_with_ctx(em_proxying_ctx* ctx, void* arg) { // should never be owned on the main thread (i.e. the argument here always // exists on the stack of the calling thread, it's never copied/malloced). assert(!f->owned); + + // The arguments have been deserialized; the only later access to `f` is the + // guarded result write in _emscripten_run_js_on_main_thread_done, so a + // canceled caller may now leave. + emscripten_proxy_release_arg(ctx); } -void _emscripten_run_js_on_main_thread_done(void* ctx, void* arg, double result) { +void _emscripten_run_js_on_main_thread_done(void* arg_ctx, + void* arg, + double result) { + em_proxying_ctx* ctx = arg_ctx; proxied_js_func_t* f = (proxied_js_func_t*)arg; - f->result = result; + // `f` lives on the caller's stack; it is gone if the caller was canceled + // while waiting (e.g. a pthread_cancel of a thread blocked in recv/poll). + if (emscripten_proxy_acquire_arg(ctx)) { + f->result = result; + } emscripten_proxy_finish(ctx); } +// PROXY_SYNC: run the JS function to completion on the target thread, then +// hand the result back through the same caller-gone guard as the async case. +static void run_js_func_sync_with_ctx(em_proxying_ctx* ctx, void* arg) { + proxied_js_func_t* f = (proxied_js_func_t*)arg; + double result = _emscripten_receive_on_main_thread_js(f->funcIndex, + f->emAsmAddr, + f->callingThread, + f->bufSize, + f->argBuffer, + NULL, + NULL); + _emscripten_run_js_on_main_thread_done(ctx, arg, result); +} + /* * The 'proxy_mode' argument to _emscripten_run_js_on_main_thread has 3 possible * values: @@ -694,7 +824,8 @@ double _emscripten_run_js_on_main_thread(int func_index, if (proxyMode == PROXY_SYNC_ASYNC) { rtn = emscripten_proxy_sync_with_ctx(q, target, run_js_func_with_ctx, &f); } else { - rtn = emscripten_proxy_sync(q, target, run_js_func, &f); + rtn = emscripten_proxy_sync_with_ctx( + q, target, run_js_func_sync_with_ctx, &f); } if (!rtn) { assert(false && "emscripten_proxy_sync_with_ctx failed"); diff --git a/system/lib/pthread/proxying_stub.c b/system/lib/pthread/proxying_stub.c index 0ebb97c2375d9..c97c3863d940d 100644 --- a/system/lib/pthread/proxying_stub.c +++ b/system/lib/pthread/proxying_stub.c @@ -25,6 +25,10 @@ void emscripten_proxy_execute_queue(em_proxying_queue* q) { abort(); } void emscripten_proxy_finish(em_proxying_ctx* ctx) { abort(); } +void emscripten_proxy_release_arg(em_proxying_ctx* ctx) { abort(); } + +bool emscripten_proxy_acquire_arg(em_proxying_ctx* ctx) { abort(); } + bool emscripten_proxy_async(em_proxying_queue* q, pthread_t target_thread, void (*func)(void*), diff --git a/test/codesize/test_codesize_minimal_pthreads.json b/test/codesize/test_codesize_minimal_pthreads.json index d0651f8052264..a25e9bcb58c35 100644 --- a/test/codesize/test_codesize_minimal_pthreads.json +++ b/test/codesize/test_codesize_minimal_pthreads.json @@ -1,68 +1,67 @@ { - "a.out.js": 6883, - "a.out.js.gz": 3422, - "a.out.nodebug.wasm": 19132, - "a.out.nodebug.wasm.gz": 8845, - "total": 26015, - "total_gz": 12267, + "a.out.js": 6870, + "a.out.js.gz": 3417, + "a.out.nodebug.wasm": 18130, + "a.out.nodebug.wasm.gz": 8407, + "total": 25000, + "total_gz": 11824, "sent": [ "a (memory)", - "b (exit)", - "c (emscripten_get_now)", - "d (_emscripten_thread_set_strongref)", - "e (_emscripten_receive_on_main_thread_js)", + "b (_emscripten_receive_on_main_thread_js)", + "c (exit)", + "d (emscripten_get_now)", + "e (_emscripten_thread_set_strongref)", "f (emscripten_runtime_keepalive_check)", "g (emscripten_resize_heap)", "h (emscripten_exit_with_live_runtime)", - "i (emscripten_check_blocking_allowed)", - "j (_emscripten_thread_mailbox_await)", - "k (_emscripten_thread_cleanup)", - "l (_emscripten_notify_mailbox_postmessage)", - "m (_emscripten_init_main_thread_js)", - "n (__pthread_create_js)" + "i (_emscripten_thread_mailbox_await)", + "j (_emscripten_thread_cleanup)", + "k (_emscripten_notify_mailbox_postmessage)", + "l (_emscripten_init_main_thread_js)", + "m (__pthread_create_js)" ], "imports": [ "a (memory)", - "b (exit)", - "c (emscripten_get_now)", - "d (_emscripten_thread_set_strongref)", - "e (_emscripten_receive_on_main_thread_js)", + "b (_emscripten_receive_on_main_thread_js)", + "c (exit)", + "d (emscripten_get_now)", + "e (_emscripten_thread_set_strongref)", "f (emscripten_runtime_keepalive_check)", "g (emscripten_resize_heap)", "h (emscripten_exit_with_live_runtime)", - "i (emscripten_check_blocking_allowed)", - "j (_emscripten_thread_mailbox_await)", - "k (_emscripten_thread_cleanup)", - "l (_emscripten_notify_mailbox_postmessage)", - "m (_emscripten_init_main_thread_js)", - "n (__pthread_create_js)" + "i (_emscripten_thread_mailbox_await)", + "j (_emscripten_thread_cleanup)", + "k (_emscripten_notify_mailbox_postmessage)", + "l (_emscripten_init_main_thread_js)", + "m (__pthread_create_js)" ], "exports": [ - "A (_emscripten_run_js_on_main_thread)", - "B (_emscripten_thread_free_data)", - "C (_emscripten_thread_exit)", - "D (_emscripten_check_mailbox)", - "E (emscripten_stack_set_limits)", - "F (_emscripten_stack_restore)", - "G (_emscripten_stack_alloc)", - "H (emscripten_stack_get_current)", - "o (__wasm_call_ctors)", - "p (add)", - "q (main)", - "r (global_val)", - "s (__indirect_function_table)", - "t (_emscripten_tls_init)", - "u (pthread_self)", - "v (_emscripten_proxy_main)", - "w (_emscripten_thread_init)", - "x (__set_thread_state)", - "y (_emscripten_thread_crashed)", - "z (_emscripten_run_js_on_main_thread_done)" + "A (_emscripten_thread_free_data)", + "B (_emscripten_thread_exit)", + "C (_emscripten_check_mailbox)", + "D (emscripten_stack_set_limits)", + "E (_emscripten_stack_restore)", + "F (_emscripten_stack_alloc)", + "G (emscripten_stack_get_current)", + "n (__wasm_call_ctors)", + "o (add)", + "p (main)", + "q (global_val)", + "r (__indirect_function_table)", + "s (_emscripten_tls_init)", + "t (pthread_self)", + "u (_emscripten_proxy_main)", + "v (_emscripten_thread_init)", + "w (__set_thread_state)", + "x (_emscripten_thread_crashed)", + "y (_emscripten_run_js_on_main_thread_done)", + "z (_emscripten_run_js_on_main_thread)" ], "funcs": [ "$__errno_location", "$__memcpy", "$__pthread_getspecific", + "$__pthread_key_create", "$__pthread_mutex_lock", "$__pthread_mutex_trylock", "$__pthread_mutex_unlock", @@ -74,7 +73,6 @@ "$__pthread_setcancelstate", "$__set_thread_state", "$__timedwait", - "$__timedwait_cp", "$__tl_lock", "$__tl_unlock", "$__vm_lock", @@ -104,20 +102,17 @@ "$a_cas_p", "$a_dec", "$a_fetch_add", - "$a_fetch_add", "$a_inc", "$a_store", "$add", "$call_callback_then_free_ctx", "$call_cancel_then_free_ctx", - "$call_then_finish_task", "$call_with_ctx", "$cancel_active_ctxs", "$cancel_ctx", "$cancel_notification", "$dispose_chunk", "$do_proxy", - "$em_proxying_ctx_deinit", "$em_task_queue_cancel", "$em_task_queue_create", "$em_task_queue_dequeue", @@ -129,28 +124,26 @@ "$emscripten_builtin_malloc", "$emscripten_futex_wait", "$emscripten_futex_wake", - "$emscripten_proxy_finish", - "$emscripten_proxy_sync_with_ctx", + "$emscripten_proxy_acquire_arg", "$emscripten_stack_get_current", "$emscripten_stack_set_limits", "$free_ctx", + "$free_ctx_pool", "$get_or_add_tasks_for_thread", "$get_tasks_for_thread", "$init_active_ctxs", "$init_file_lock", "$init_mparams", - "$lock", "$main", "$nodtor", - "$pthread_cond_signal", - "$pthread_mutex_destroy", "$pthread_setspecific", "$receive_notification", - "$remove_active_ctx", "$run_js_func", + "$run_js_func_sync_with_ctx", "$run_js_func_with_ctx", "$sbrk", - "$undo", - "$unlock" + "$sync_ctx_complete", + "$sync_ctx_free", + "$undo" ] } diff --git a/test/codesize/test_codesize_minimal_pthreads_memgrowth.json b/test/codesize/test_codesize_minimal_pthreads_memgrowth.json index c258456676ca6..6ce303361bd01 100644 --- a/test/codesize/test_codesize_minimal_pthreads_memgrowth.json +++ b/test/codesize/test_codesize_minimal_pthreads_memgrowth.json @@ -1,68 +1,67 @@ { - "a.out.js": 7341, - "a.out.js.gz": 3639, - "a.out.nodebug.wasm": 19133, - "a.out.nodebug.wasm.gz": 8846, - "total": 26474, - "total_gz": 12485, + "a.out.js": 7329, + "a.out.js.gz": 3634, + "a.out.nodebug.wasm": 18131, + "a.out.nodebug.wasm.gz": 8410, + "total": 25460, + "total_gz": 12044, "sent": [ "a (memory)", - "b (exit)", - "c (emscripten_get_now)", - "d (_emscripten_thread_set_strongref)", - "e (_emscripten_receive_on_main_thread_js)", + "b (_emscripten_receive_on_main_thread_js)", + "c (exit)", + "d (emscripten_get_now)", + "e (_emscripten_thread_set_strongref)", "f (emscripten_runtime_keepalive_check)", "g (emscripten_resize_heap)", "h (emscripten_exit_with_live_runtime)", - "i (emscripten_check_blocking_allowed)", - "j (_emscripten_thread_mailbox_await)", - "k (_emscripten_thread_cleanup)", - "l (_emscripten_notify_mailbox_postmessage)", - "m (_emscripten_init_main_thread_js)", - "n (__pthread_create_js)" + "i (_emscripten_thread_mailbox_await)", + "j (_emscripten_thread_cleanup)", + "k (_emscripten_notify_mailbox_postmessage)", + "l (_emscripten_init_main_thread_js)", + "m (__pthread_create_js)" ], "imports": [ "a (memory)", - "b (exit)", - "c (emscripten_get_now)", - "d (_emscripten_thread_set_strongref)", - "e (_emscripten_receive_on_main_thread_js)", + "b (_emscripten_receive_on_main_thread_js)", + "c (exit)", + "d (emscripten_get_now)", + "e (_emscripten_thread_set_strongref)", "f (emscripten_runtime_keepalive_check)", "g (emscripten_resize_heap)", "h (emscripten_exit_with_live_runtime)", - "i (emscripten_check_blocking_allowed)", - "j (_emscripten_thread_mailbox_await)", - "k (_emscripten_thread_cleanup)", - "l (_emscripten_notify_mailbox_postmessage)", - "m (_emscripten_init_main_thread_js)", - "n (__pthread_create_js)" + "i (_emscripten_thread_mailbox_await)", + "j (_emscripten_thread_cleanup)", + "k (_emscripten_notify_mailbox_postmessage)", + "l (_emscripten_init_main_thread_js)", + "m (__pthread_create_js)" ], "exports": [ - "A (_emscripten_run_js_on_main_thread)", - "B (_emscripten_thread_free_data)", - "C (_emscripten_thread_exit)", - "D (_emscripten_check_mailbox)", - "E (emscripten_stack_set_limits)", - "F (_emscripten_stack_restore)", - "G (_emscripten_stack_alloc)", - "H (emscripten_stack_get_current)", - "o (__wasm_call_ctors)", - "p (add)", - "q (main)", - "r (global_val)", - "s (__indirect_function_table)", - "t (_emscripten_tls_init)", - "u (pthread_self)", - "v (_emscripten_proxy_main)", - "w (_emscripten_thread_init)", - "x (__set_thread_state)", - "y (_emscripten_thread_crashed)", - "z (_emscripten_run_js_on_main_thread_done)" + "A (_emscripten_thread_free_data)", + "B (_emscripten_thread_exit)", + "C (_emscripten_check_mailbox)", + "D (emscripten_stack_set_limits)", + "E (_emscripten_stack_restore)", + "F (_emscripten_stack_alloc)", + "G (emscripten_stack_get_current)", + "n (__wasm_call_ctors)", + "o (add)", + "p (main)", + "q (global_val)", + "r (__indirect_function_table)", + "s (_emscripten_tls_init)", + "t (pthread_self)", + "u (_emscripten_proxy_main)", + "v (_emscripten_thread_init)", + "w (__set_thread_state)", + "x (_emscripten_thread_crashed)", + "y (_emscripten_run_js_on_main_thread_done)", + "z (_emscripten_run_js_on_main_thread)" ], "funcs": [ "$__errno_location", "$__memcpy", "$__pthread_getspecific", + "$__pthread_key_create", "$__pthread_mutex_lock", "$__pthread_mutex_trylock", "$__pthread_mutex_unlock", @@ -74,7 +73,6 @@ "$__pthread_setcancelstate", "$__set_thread_state", "$__timedwait", - "$__timedwait_cp", "$__tl_lock", "$__tl_unlock", "$__vm_lock", @@ -104,20 +102,17 @@ "$a_cas_p", "$a_dec", "$a_fetch_add", - "$a_fetch_add", "$a_inc", "$a_store", "$add", "$call_callback_then_free_ctx", "$call_cancel_then_free_ctx", - "$call_then_finish_task", "$call_with_ctx", "$cancel_active_ctxs", "$cancel_ctx", "$cancel_notification", "$dispose_chunk", "$do_proxy", - "$em_proxying_ctx_deinit", "$em_task_queue_cancel", "$em_task_queue_create", "$em_task_queue_dequeue", @@ -129,28 +124,26 @@ "$emscripten_builtin_malloc", "$emscripten_futex_wait", "$emscripten_futex_wake", - "$emscripten_proxy_finish", - "$emscripten_proxy_sync_with_ctx", + "$emscripten_proxy_acquire_arg", "$emscripten_stack_get_current", "$emscripten_stack_set_limits", "$free_ctx", + "$free_ctx_pool", "$get_or_add_tasks_for_thread", "$get_tasks_for_thread", "$init_active_ctxs", "$init_file_lock", "$init_mparams", - "$lock", "$main", "$nodtor", - "$pthread_cond_signal", - "$pthread_mutex_destroy", "$pthread_setspecific", "$receive_notification", - "$remove_active_ctx", "$run_js_func", + "$run_js_func_sync_with_ctx", "$run_js_func_with_ctx", "$sbrk", - "$undo", - "$unlock" + "$sync_ctx_complete", + "$sync_ctx_free", + "$undo" ] } diff --git a/test/pthread/test_pthread_proxying_canceled_caller.c b/test/pthread/test_pthread_proxying_canceled_caller.c new file mode 100644 index 0000000000000..f09398176a90f --- /dev/null +++ b/test/pthread/test_pthread_proxying_canceled_caller.c @@ -0,0 +1,148 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * A thread blocked in emscripten_proxy_sync[_with_ctx] is canceled while its + * work is (a) in progress on the target and (b) handed a ctx the target has + * not finished yet. The caller must exit with PTHREAD_CANCELED, but only once + * the target is done with the caller-owned argument (its stack), i.e. when the + * work completes or the ctx is finished, or (c) the task releases the argument + * with emscripten_proxy_release_arg, letting the canceled caller exit early, + * after which emscripten_proxy_acquire_arg must fail. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +em_proxying_queue* q; +pthread_t target; + +// The target runs its event loop, which is where proxied work is dispatched. +void* target_main(void* arg) { emscripten_exit_with_live_runtime(); } + +void stop_target(void* arg) { emscripten_runtime_keepalive_pop(); } + +// (a) Work in progress on the target when the caller is canceled. The target +// keeps reading the caller's stack argument until released, so the canceled +// caller must not have unwound yet. +_Atomic bool slow_started, slow_release, slow_done; + +void slow(void* arg) { + slow_started = true; + while (!slow_release) { + usleep(1000); + } + assert(*(int*)arg == 42 && "caller stack freed while target still reads it"); + slow_done = true; +} + +void* sync_caller(void* arg) { + int local = 42; + emscripten_proxy_sync(q, target, slow, &local); + assert(false && "should have been canceled"); + return NULL; +} + +// (b) The target returns from the proxied function without finishing the ctx; +// the canceled caller is held until the ctx is finished. +em_proxying_ctx* _Atomic stashed; +_Atomic bool ctx_caller_exiting; + +void stash(em_proxying_ctx* ctx, void* arg) { stashed = ctx; } + +void note_exit(void* arg) { ctx_caller_exiting = true; } + +void* ctx_caller(void* arg) { + int local = 42; + pthread_cleanup_push(note_exit, NULL); + emscripten_proxy_sync_with_ctx(q, target, stash, &local); + pthread_cleanup_pop(0); + assert(false && "should have been canceled"); + return NULL; +} + +void finish_stashed(void* arg) { emscripten_proxy_finish(stashed); } + +// (c) The task releases `arg` early: a canceled caller may then exit before +// the task is finished, after which `arg` can no longer be reacquired. +em_proxying_ctx* _Atomic released; + +void release_early(em_proxying_ctx* ctx, void* arg) { + // The caller is still waiting, so the arg can be acquired and accessed. + assert(emscripten_proxy_acquire_arg(ctx)); + assert(*(int*)arg == 42); + emscripten_proxy_release_arg(ctx); + released = ctx; +} + +void* release_caller(void* arg) { + int local = 42; + emscripten_proxy_sync_with_ctx(q, target, release_early, &local); + assert(false && "should have been canceled"); + return NULL; +} + +void finish_released(void* arg) { + // The canceled caller has exited, so the arg can no longer be acquired. + assert(!emscripten_proxy_acquire_arg(released)); + emscripten_proxy_finish(released); +} + +void noop(void* arg) {} + +int main(void) { + q = em_proxying_queue_create(); + assert(pthread_create(&target, NULL, target_main, NULL) == 0); + + pthread_t caller; + void* ret; + + assert(pthread_create(&caller, NULL, sync_caller, NULL) == 0); + while (!slow_started) + usleep(1000); + assert(pthread_cancel(caller) == 0); + usleep(20000); // the cancel must not complete while `slow` still runs + assert(!slow_done); + slow_release = true; + assert(pthread_join(caller, &ret) == 0); + assert(ret == PTHREAD_CANCELED); + assert(slow_done); + + assert(pthread_create(&caller, NULL, ctx_caller, NULL) == 0); + while (!stashed) + usleep(1000); + assert(pthread_cancel(caller) == 0); + usleep(20000); // the cancel must not complete while the ctx is unfinished + assert(!ctx_caller_exiting); + assert(emscripten_proxy_async(q, target, finish_stashed, NULL)); + assert(pthread_join(caller, &ret) == 0); + assert(ret == PTHREAD_CANCELED); + assert(ctx_caller_exiting); + + assert(pthread_create(&caller, NULL, release_caller, NULL) == 0); + while (!released) + usleep(1000); + assert(pthread_cancel(caller) == 0); + // The caller can exit even though the ctx is not yet finished. + assert(pthread_join(caller, &ret) == 0); + assert(ret == PTHREAD_CANCELED); + assert(emscripten_proxy_async(q, target, finish_released, NULL)); + + // The queue is still healthy for ordinary work. + assert(emscripten_proxy_sync(q, target, noop, NULL)); + + assert(emscripten_proxy_async(q, target, stop_target, NULL)); + assert(pthread_join(target, NULL) == 0); + em_proxying_queue_destroy(q); + printf("done\n"); + return 0; +} diff --git a/test/sockets/test_epoll_cancel.c b/test/sockets/test_epoll_cancel.c new file mode 100644 index 0000000000000..65b9dbc0d0bdf --- /dev/null +++ b/test/sockets/test_epoll_cancel.c @@ -0,0 +1,85 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Threads blocked indefinitely in epoll_wait() and poll() on a socket are + * canceled with pthread_cancel. Each must exit with PTHREAD_CANCELED, leaving + * its readiness listener registered on the main thread. A datagram then fires + * those orphaned listeners - resolving proxied waits whose callers are gone - + * which must be harmless, and the socket must still be usable afterwards. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int rx, tx, ep; +static struct sockaddr_in addr; + +static void* epoll_waiter(void* arg) { + struct epoll_event out; + epoll_wait(ep, &out, 1, -1); + assert(0 && "epoll_wait should have been canceled"); + return NULL; +} + +static void* poll_waiter(void* arg) { + struct pollfd pfd = {.fd = rx, .events = POLLIN}; + poll(&pfd, 1, -1); + assert(0 && "poll should have been canceled"); + return NULL; +} + +int main(void) { + rx = socket(AF_INET, SOCK_DGRAM, 0); + tx = socket(AF_INET, SOCK_DGRAM, 0); + assert(rx >= 0 && tx >= 0); + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + assert(bind(rx, (struct sockaddr*)&addr, sizeof(addr)) == 0); + socklen_t l = sizeof(addr); + assert(getsockname(rx, (struct sockaddr*)&addr, &l) == 0); + + ep = epoll_create1(0); + assert(ep >= 0); + struct epoll_event ev = {.events = EPOLLIN}; + ev.data.fd = rx; + assert(epoll_ctl(ep, EPOLL_CTL_ADD, rx, &ev) == 0); + + pthread_t t1, t2; + assert(pthread_create(&t1, NULL, epoll_waiter, NULL) == 0); + assert(pthread_create(&t2, NULL, poll_waiter, NULL) == 0); + usleep(100000); // let both park in their waits first + + void* ret; + assert(pthread_cancel(t1) == 0); + assert(pthread_join(t1, &ret) == 0 && ret == PTHREAD_CANCELED); + assert(pthread_cancel(t2) == 0); + assert(pthread_join(t2, &ret) == 0 && ret == PTHREAD_CANCELED); + + // Fire the listeners the canceled waits left behind. + assert(sendto(tx, "ping", 4, 0, (struct sockaddr*)&addr, sizeof(addr)) == 4); + usleep(100000); // let the late completions run on the main thread + + struct epoll_event out; + assert(epoll_wait(ep, &out, 1, -1) == 1 && out.data.fd == rx); + char buf[4]; + assert(recvfrom(rx, buf, sizeof(buf), 0, NULL, NULL) == 4); + assert(memcmp(buf, "ping", 4) == 0); + + close(ep); + close(rx); + close(tx); + printf("done\n"); + return 0; +} diff --git a/test/test_core.py b/test/test_core.py index 110dacf458821..08dc94e502ce3 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -2616,6 +2616,14 @@ def test_pthread_proxying_canceled_work(self): 'pthread/test_pthread_proxying_canceled_work.c', interleaved_output=False) + @requires_pthreads + def test_pthread_proxying_canceled_caller(self): + # pthread_cancel of a thread blocked in a sync proxy call, both while the + # target still runs the work and after the target has been handed the ctx. + self.set_setting('PROXY_TO_PTHREAD') + self.set_setting('EXIT_RUNTIME') + self.do_runf('pthread/test_pthread_proxying_canceled_caller.c', 'done\n') + @requires_pthreads @flaky('https://github.com/emscripten-core/emscripten/issues/19795') def test_pthread_proxying_refcount(self): diff --git a/test/test_sockets_node.py b/test/test_sockets_node.py index 16ec7db2815aa..09dac7f6c9327 100644 --- a/test/test_sockets_node.py +++ b/test/test_sockets_node.py @@ -100,12 +100,12 @@ def _run_against_echo_server(self, src): # are proxied to the main thread: with PROXY_TO_PTHREAD, main() runs on a # worker and every socket call funnels to the main thread where node:net lives. @also_with_proxy_to_pthread - def test_noderawsockets_echo(self): + def test_echo(self): # With -sNODERAWSOCKETS the client does a non-blocking connect, send and # recv over a real OS socket against a loopback echo server we run here. self._run_against_echo_server('sockets/test_tcp_echo.c') - def test_noderawsockets_client_bind(self): + def test_client_bind(self): # A client that bind()s an explicit source port has it honored by connect(), # and the plain client path never realizes a private tcp_wrap handle. We # allocate a free source port here and pass it alongside the echo server's. @@ -126,22 +126,22 @@ def test_noderawsockets_client_bind(self): server.server_close() thread.join() - def test_noderawsockets_connect_getsockname(self): + def test_connect_getsockname(self): # getsockname() immediately after a non-blocking connect() on an unbound # client reports the ephemeral source port synchronously (kernel semantics: # the port is assigned at connect(), not when the connection completes). self.do_runf('sockets/test_tcp_connect_getsockname.c', 'done\n', cflags=['-sNODERAWSOCKETS']) - def test_noderawsockets_client_semantics(self): + def test_client_semantics(self): # EISCONN on a second connect, shutdown(SHUT_WR) leaving reads working, # EPIPE on a write after that, and POLLHUP after a full shutdown(SHUT_RDWR). self._run_against_echo_server('sockets/test_tcp_client_semantics.c') - def test_noderawsockets_refused(self): + def test_refused(self): # A connect to a loopback port with nothing listening reports ECONNREFUSED. self.do_runf('sockets/test_tcp_refused.c', 'done\n', cflags=['-sNODERAWSOCKETS']) - def test_noderawsockets_backpressure(self): + def test_backpressure(self): # A sink server that accepts but never reads, so the client's writes fill # the buffers and send() reports EAGAIN rather than buffering unboundedly. done = threading.Event() @@ -163,14 +163,14 @@ def handle(self): thread.join() @also_with_proxy_to_pthread - def test_noderawsockets_server(self): + def test_server(self): # Self-contained loopback accept+echo, exercising bind(:0)+getsockname # (synchronous ephemeral port), listen, accept, non-blocking connect, send # and recv over real OS sockets via the tcp_wrap server path. self.do_runf('sockets/test_tcp_server.c', 'done\n', cflags=['-sNODERAWSOCKETS']) @also_with_proxy_to_pthread - def test_noderawsockets_peek(self): + def test_peek(self): # recv(MSG_PEEK) must leave the data buffered: a peek returns the bytes, the # socket stays readable, and the following plain recv returns them again. self.do_runf('sockets/test_tcp_peek.c', 'done\n', cflags=['-sNODERAWSOCKETS']) @@ -179,39 +179,39 @@ def test_noderawsockets_peek(self): # filesystem, which only stays coherent with the program's own file syscalls # (bind's parent dir, getsockname, unlink) when the FS is the host FS. @also_with_proxy_to_pthread - def test_noderawsockets_unix_server(self): + def test_unix_server(self): # Self-contained named AF_UNIX (pathname) loopback accept+echo: bind(path), # listen, getsockname (the bound path), accept, getpeername, non-blocking # connect-by-path, send and recv over a real node pipe. self.do_runf('sockets/test_unix_server.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sNODERAWFS']) - def test_noderawsockets_unix_refused(self): + def test_unix_refused(self): # A connect to an AF_UNIX path with no socket file reports ENOENT. self.do_runf('sockets/test_unix_refused.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sNODERAWFS']) - def test_noderawsockets_unix_bind_inuse(self): + def test_unix_bind_inuse(self): # Binding an already-bound AF_UNIX path fails synchronously with EADDRINUSE. self.do_runf('sockets/test_unix_bind_inuse.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sNODERAWFS']) - def test_noderawsockets_server_autobind(self): + def test_server_autobind(self): # listen() without a prior bind() must auto-bind an ephemeral port and # getsockname() must report it (POSIX), then accept+echo as usual. self.do_runf('sockets/test_tcp_server.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-DNO_EXPLICIT_BIND']) - def test_noderawsockets_tcp_ipv6(self): + def test_tcp_ipv6(self): # Self-contained IPv6 TCP loopback accept+echo over ::1: bind(:0)+getsockname, # listen, accept, non-blocking connect, send/recv on AF_INET6 sockets. if not HAS_IPV6_LOOPBACK: self.skipTest('no IPv6 loopback available') self.do_runf('sockets/test_tcp_ipv6.c', 'done\n', cflags=['-sNODERAWSOCKETS']) - def test_noderawsockets_udp_ipv6(self): + def test_udp_ipv6(self): # Self-contained IPv6 UDP loopback echo over ::1 on AF_INET6 sockets. if not HAS_IPV6_LOOPBACK: self.skipTest('no IPv6 loopback available') self.do_runf('sockets/test_udp_ipv6.c', 'done\n', cflags=['-sNODERAWSOCKETS']) - def test_noderawsockets_epoll_socket_blocking(self): + def test_epoll_socket_blocking(self): # A blocking epoll_wait() on a socket is woken by an incoming datagram # through the unified readiness wait-queue (the SOCKFS.emit bridge), with # main() proxied to a worker so the wait can suspend. @@ -219,47 +219,54 @@ def test_noderawsockets_epoll_socket_blocking(self): cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) @requires_jspi_node - def test_noderawsockets_epoll_socket_blocking_jspi(self): + def test_epoll_socket_blocking_jspi(self): # Same, but the blocking epoll_wait() suspends the wasm stack under JSPI. self.do_runf('sockets/test_epoll_socket_blocking.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) - def test_noderawsockets_epoll_rdhup(self): + def test_epoll_cancel(self): + # pthread_cancel of threads blocked in epoll_wait(-1) and poll(-1) exits + # them with PTHREAD_CANCELED; the readiness listeners they left behind are + # then fired by a real datagram and must complete harmlessly. + self.do_runf('sockets/test_epoll_cancel.c', 'done\n', + cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) + + def test_epoll_rdhup(self): # A blocking epoll_wait reports EPOLLRDHUP when the TCP peer half-closes its # write side (FIN), distinct from a full EPOLLHUP, and only when requested. self.do_runf('sockets/test_epoll_rdhup.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) @requires_jspi_node - def test_noderawsockets_epoll_rdhup_jspi(self): + def test_epoll_rdhup_jspi(self): # Same, but the blocking calls suspend the wasm stack under JSPI. self.do_runf('sockets/test_epoll_rdhup.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) @also_with_proxy_to_pthread - def test_noderawsockets_udp(self): + def test_udp(self): # Self-contained loopback UDP echo: the server binds(:0)+getsockname for its # ephemeral port, the client sends a datagram, the server echoes it back. self.do_runf('sockets/test_udp_echo.c', 'done\n', cflags=['-sNODERAWSOCKETS']) - def test_noderawsockets_udp_recvmsg(self): + def test_udp_recvmsg(self): # recvmsg scatters a datagram across multiple iovecs at the right offsets # and updates msg_namelen/msg_controllen/msg_flags in the caller's msghdr. self.do_runf('sockets/test_udp_recvmsg.c', 'done\n', cflags=['-sNODERAWSOCKETS']) - def test_noderawsockets_mmsg(self): + def test_mmsg(self): # sendmmsg batches two datagrams out, recvmmsg receives them back in one # call, updating msg_len per message. self.do_runf('sockets/test_udp_mmsg.c', 'done\n', cflags=['-sNODERAWSOCKETS']) @also_with_proxy_to_pthread - def test_noderawsockets_udp_connect(self): + def test_udp_connect(self): # Connected UDP: sendto() with an address gives EISCONN, send() reaches the # peer, and datagrams from a non-peer socket are filtered out. self.do_runf('sockets/test_udp_connect.c', 'done\n', cflags=['-sNODERAWSOCKETS']) @also_with_proxy_to_pthread - def test_noderawsockets_udp_sockopts(self): + def test_udp_sockopts(self): # UDP multicast socket options: IP_MULTICAST_TTL/LOOP and their IPv6 # counterparts round-trip through set/getsockopt, with POSIX defaults # readable before any set. EXIT_RUNTIME so the plain synchronous main() @@ -268,7 +275,7 @@ def test_noderawsockets_udp_sockopts(self): self.do_runf('sockets/test_udp_sockopts.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME']) @also_with_proxy_to_pthread - def test_noderawsockets_socket_options(self): + def test_socket_options(self): # Socket metadata/options on a fresh socket: fstat reports S_ISSOCK, SO_TYPE # reports the socket type, and SO_LINGER round-trips a struct linger. self.do_runf('sockets/test_socket_options.c', 'done\n',