Skip to content
Open
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
92 changes: 92 additions & 0 deletions api-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -2230,6 +2321,7 @@ static void new_context_low_memory(void)

int main(void)
{
discard_pending_jobs();
cfunctions();
sync_call();
async_call();
Expand Down
22 changes: 22 additions & 0 deletions list.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
20 changes: 20 additions & 0 deletions quickjs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions quickjs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down