diff --git a/api-test.c b/api-test.c index 2d5f304e2..3d2ad5a7e 100644 --- a/api-test.c +++ b/api-test.c @@ -2195,6 +2195,97 @@ void private_symbols(void) JS_FreeRuntime(rt); } +static int discarded_job_calls; +static int discarded_job_finalizers; + +static JSValue discard_job_callback(JSContext *ctx, int argc, JSValueConst *argv) +{ + discarded_job_calls++; + return JS_UNDEFINED; +} + +static void discard_job_finalizer(JSRuntime *rt, JSValueConst val) +{ + JSContext *ctx = JS_GetRuntimeOpaque(rt); + + discarded_job_finalizers++; + if (ctx) + assert(JS_EnqueueJob(ctx, discard_job_callback, 0, NULL) == 0); +} + +static void discard_pending_jobs(void) +{ + JSRuntime *rt = new_runtime(); + JSContext *ctx = JS_NewContext(rt); + JSContext *job_ctx = NULL; + JSClassID class_id = 0; + JSClassDef def = { "DiscardJobArgument", .finalizer = discard_job_finalizer }; + JSValue args[2]; + JSMemoryUsage before, after; + int i; + + assert(rt && ctx); + assert(JS_DiscardPendingJobs(rt) == 0); + JS_NewClassID(rt, &class_id); + assert(JS_NewClass(rt, class_id, &def) == 0); + JS_ComputeMemoryUsage(rt, &before); + for (i = 0; i < 2; i++) { + args[0] = JS_NewObjectClass(ctx, class_id); + args[1] = JS_NewString(ctx, "retained job argument"); + assert(!JS_IsException(args[0])); + assert(!JS_IsException(args[1])); + assert(JS_EnqueueJob(ctx, discard_job_callback, 2, (JSValueConst *)args) == 0); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + } + assert(discarded_job_finalizers == 0); + assert(JS_IsJobPending(rt)); + assert(JS_DiscardPendingJobs(rt) == 2); + assert(!JS_IsJobPending(rt)); + assert(discarded_job_calls == 0); + assert(discarded_job_finalizers == 2); + assert(JS_DiscardPendingJobs(rt) == 0); + JS_ComputeMemoryUsage(rt, &after); + assert(after.malloc_count == before.malloc_count); + assert(after.memory_used_size == before.memory_used_size); + + assert(JS_EnqueueJob(ctx, discard_job_callback, 0, NULL) == 0); + assert(JS_ExecutePendingJob(rt, &job_ctx) == 1); + assert(job_ctx == ctx); + assert(discarded_job_calls == 1); + assert(JS_ExecutePendingJob(rt, &job_ctx) == 0); + assert(JS_DiscardPendingJobs(rt) == 0); + + /* Finalizers may enqueue new jobs, but discarding must only consume the + jobs present at entry. Exercise both singleton and multi-entry queues. */ + JS_SetRuntimeOpaque(rt, ctx); + for (i = 1; i <= 2; i++) { + int j, calls = discarded_job_calls; + int finalizers = discarded_job_finalizers; + + for (j = 0; j < i; j++) { + args[0] = JS_NewObjectClass(ctx, class_id); + assert(!JS_IsException(args[0])); + assert(JS_EnqueueJob(ctx, discard_job_callback, 1, (JSValueConst *)args) == 0); + JS_FreeValue(ctx, args[0]); + } + assert(JS_DiscardPendingJobs(rt) == (size_t)i); + assert(discarded_job_finalizers == finalizers + i); + assert(discarded_job_calls == calls); + assert(JS_IsJobPending(rt)); + for (j = 0; j < i; j++) { + assert(JS_ExecutePendingJob(rt, &job_ctx) == 1); + assert(job_ctx == ctx); + } + assert(discarded_job_calls == calls + i); + assert(JS_ExecutePendingJob(rt, &job_ctx) == 0); + assert(JS_DiscardPendingJobs(rt) == 0); + } + JS_SetRuntimeOpaque(rt, NULL); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + // https://github.com/quickjs-ng/quickjs/issues/1181 // JS_NewContext() must fail cleanly, not crash or leak, whichever of its // allocations runs out of memory. @@ -2230,6 +2321,7 @@ static void new_context_low_memory(void) int main(void) { + discard_pending_jobs(); cfunctions(); sync_call(); async_call(); diff --git a/list.h b/list.h index b8dd71681..dc4f7813d 100644 --- a/list.h +++ b/list.h @@ -86,6 +86,28 @@ static inline int list_empty(struct list_head *el) return el->next == el; } +/* move elements from 'el' through the tail of 'src' into 'dst' */ +static inline void list_split(struct list_head *src, struct list_head *el, + struct list_head *dst) +{ + dst->prev = src->prev; + dst->prev->next = dst; + dst->next = el; + src->prev = el->prev; + src->prev->next = src; + el->prev = dst; +} + +/* move all elements from 'src' into 'dst', leaving 'src' empty */ +static inline void list_move(struct list_head *src, struct list_head *dst) +{ + if (list_empty(src)) { + init_list_head(dst); + } else { + list_split(src, src->next, dst); + } +} + #define list_for_each(el, head) \ for(el = (head)->next; el != (head); el = el->next) diff --git a/quickjs.c b/quickjs.c index ed65ce420..2b1835b4a 100644 --- a/quickjs.c +++ b/quickjs.c @@ -2508,6 +2508,26 @@ int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, return 0; } +size_t JS_DiscardPendingJobs(JSRuntime *rt) +{ + size_t count = 0; + struct list_head jobs; + JSJobEntry *e; + int i; + + /* Finalizers may enqueue jobs while arguments are released. */ + list_move(&rt->job_list, &jobs); + while (!list_empty(&jobs)) { + e = list_entry(jobs.next, JSJobEntry, link); + list_del(&e->link); + for (i = 0; i < e->argc; i++) + JS_FreeValueRT(rt, e->argv[i]); + js_free_rt(rt, e); + count++; + } + return count; +} + bool JS_IsJobPending(JSRuntime *rt) { return !list_empty(&rt->job_list); diff --git a/quickjs.h b/quickjs.h index 7db40213c..05a8396d7 100644 --- a/quickjs.h +++ b/quickjs.h @@ -1250,6 +1250,15 @@ typedef JSValue JSJobFunc(JSContext *ctx, int argc, JSValueConst *argv); JS_EXTERN int JS_EnqueueJob(JSContext *ctx, JSJobFunc *job_func, int argc, JSValueConst *argv); +/* Discard the jobs queued at entry without executing their callbacks. Releases + their argument references and entries, but does not free contexts or the + runtime. Returns the number of discarded jobs; the runtime remains usable. + Call only while no JavaScript or job is executing and with exclusive access + to the runtime. Releasing arguments may invoke native finalizers; these must + not execute JavaScript or reenter this function. Jobs enqueued by finalizers + remain pending and are not included in the returned count. This does not + settle promises whose reactions are discarded. */ +JS_EXTERN size_t JS_DiscardPendingJobs(JSRuntime *rt); JS_EXTERN bool JS_IsJobPending(JSRuntime *rt); JS_EXTERN JSContext *JS_GetPendingJobContext(JSRuntime *rt); JS_EXTERN int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx);