[gjs/ewlsh/implicit-mainloop: 1/3] Implement custom GSource to handle promise queueing
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/implicit-mainloop: 1/3] Implement custom GSource to handle promise queueing
- Date: Sun, 5 Sep 2021 04:10:10 +0000 (UTC)
commit 71c481cc483e498365f4baca57f5581ecb8a3c43
Author: Evan Welsh <contact evanwelsh com>
Date: Sat Sep 4 19:58:31 2021 -0700
Implement custom GSource to handle promise queueing
gjs/context-private.h | 5 +-
gjs/context.cpp | 69 ++++++++--------
gjs/promise.cpp | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++
gjs/promise.h | 31 ++++++++
meson.build | 1 +
5 files changed, 282 insertions(+), 36 deletions(-)
---
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 75b04bf0..8004dbc0 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -17,6 +17,7 @@
#include <utility> // for pair
#include <vector>
+#include <gio/gio.h>
#include <glib-object.h>
#include <glib.h>
@@ -78,7 +79,8 @@ class GjsContextPrivate : public JS::JobQueue {
std::vector<std::string> m_args;
JobQueueStorage m_job_queue;
- unsigned m_idle_drain_handler;
+ GSource* m_promise_queue_source;
+ GCancellable* m_promise_queue_source_cancellable;
std::vector<std::pair<DestroyNotify, void*>> m_destroy_notifications;
std::vector<Gjs::Closure::Ptr> m_async_closures;
@@ -134,7 +136,6 @@ class GjsContextPrivate : public JS::JobQueue {
class SavedQueue;
void start_draining_job_queue(void);
void stop_draining_job_queue(void);
- static gboolean drain_job_queue_idle_handler(void* data);
uint8_t handle_exit_code(const char* type, const char* identifier,
GError** error);
diff --git a/gjs/context.cpp b/gjs/context.cpp
index d8992a58..83d5bedc 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -81,6 +81,7 @@
#include "gjs/objectbox.h"
#include "gjs/profiler-private.h"
#include "gjs/profiler.h"
+#include "gjs/promise.h"
#include "gjs/text-encoding.h"
#include "modules/modules.h"
#include "util/log.h"
@@ -320,9 +321,11 @@ gjs_context_class_init(GjsContextClass *klass)
char *priv_typelib_dir = g_build_filename (PKGLIBDIR, "girepository-1.0", NULL);
#endif
g_irepository_prepend_search_path(priv_typelib_dir);
- g_free (priv_typelib_dir);
+ g_free(priv_typelib_dir);
}
+ gjs_register_native_module("_promiseNative",
+ gjs_define_native_promise_stuff);
gjs_register_native_module("_byteArrayNative", gjs_define_byte_array_stuff);
gjs_register_native_module("_encodingNative",
gjs_define_text_encoding_stuff);
@@ -532,7 +535,6 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
m_cx(cx),
m_owner_thread(std::this_thread::get_id()),
m_environment_preparer(cx) {
-
JS_SetGCCallback(
cx,
[](JSContext*, JSGCStatus status, JS::GCReason reason, void* data) {
@@ -894,32 +896,33 @@ bool GjsContextPrivate::should_exit(uint8_t* exit_code_p) const {
}
void GjsContextPrivate::start_draining_job_queue(void) {
- if (!m_idle_drain_handler) {
+ if (!m_promise_queue_source) {
gjs_debug(GJS_DEBUG_CONTEXT, "Starting promise job queue handler");
- m_idle_drain_handler = g_idle_add_full(
- G_PRIORITY_DEFAULT, drain_job_queue_idle_handler, this, nullptr);
+
+ m_promise_queue_source_cancellable = g_cancellable_new();
+ m_promise_queue_source = gjs_promise_job_queue_source_new(
+ this, m_promise_queue_source_cancellable);
+
+ gjs_promise_job_queue_source_attach(m_promise_queue_source);
}
+
+ gjs_promise_job_queue_source_wakeup(m_promise_queue_source);
}
void GjsContextPrivate::stop_draining_job_queue(void) {
m_draining_job_queue = false;
- if (m_idle_drain_handler) {
- gjs_debug(GJS_DEBUG_CONTEXT, "Stopping promise job queue handler");
- g_source_remove(m_idle_drain_handler);
- m_idle_drain_handler = 0;
+
+ if (m_promise_queue_source_cancellable) {
+ gjs_debug(GJS_DEBUG_CONTEXT, "Cancelling promise job queue handler");
+ g_cancellable_cancel(m_promise_queue_source_cancellable);
}
-}
-gboolean GjsContextPrivate::drain_job_queue_idle_handler(void* data) {
- gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler");
- auto* gjs = static_cast<GjsContextPrivate*>(data);
- gjs->runJobs(gjs->context());
- /* Uncatchable exceptions are swallowed here - no way to get a handle on
- * the main loop to exit it from this idle handler */
- gjs_debug(GJS_DEBUG_CONTEXT, "Promise job queue handler finished");
- g_assert(gjs->empty() && gjs->m_idle_drain_handler == 0 &&
- "GjsContextPrivate::runJobs() should have emptied queue");
- return G_SOURCE_REMOVE;
+ if (m_promise_queue_source) {
+ gjs_debug(GJS_DEBUG_CONTEXT, "Destroying promise job queue handler");
+ gjs_promise_job_queue_source_remove(m_promise_queue_source);
+ m_promise_queue_source = nullptr;
+ m_promise_queue_source_cancellable = nullptr;
+ }
}
JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) {
@@ -942,11 +945,6 @@ bool GjsContextPrivate::enqueuePromiseJob(JSContext* cx [[maybe_unused]],
gjs_debug_object(job).c_str(), gjs_debug_object(promise).c_str(),
gjs_debug_object(allocation_site).c_str());
- if (m_idle_drain_handler)
- g_assert(!empty());
- else
- g_assert(empty());
-
if (!m_job_queue.append(job)) {
JS_ReportOutOfMemory(m_cx);
return false;
@@ -1031,8 +1029,8 @@ bool GjsContextPrivate::run_jobs_fallible(void) {
}
}
+ m_draining_job_queue = false;
m_job_queue.clear();
- stop_draining_job_queue();
JS::JobQueueIsEmpty(m_cx);
return retval;
}
@@ -1041,14 +1039,12 @@ class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue {
private:
GjsContextPrivate* m_gjs;
JS::PersistentRooted<JobQueueStorage> m_queue;
- bool m_idle_was_pending : 1;
bool m_was_draining : 1;
public:
explicit SavedQueue(GjsContextPrivate* gjs)
: m_gjs(gjs),
m_queue(gjs->m_cx, std::move(gjs->m_job_queue)),
- m_idle_was_pending(gjs->m_idle_drain_handler != 0),
m_was_draining(gjs->m_draining_job_queue) {
gjs_debug(GJS_DEBUG_CONTEXT, "Pausing job queue");
gjs->stop_draining_job_queue();
@@ -1058,8 +1054,7 @@ class GjsContextPrivate::SavedQueue : public JS::JobQueue::SavedJobQueue {
gjs_debug(GJS_DEBUG_CONTEXT, "Unpausing job queue");
m_gjs->m_job_queue = std::move(m_queue.get());
m_gjs->m_draining_job_queue = m_was_draining;
- if (m_idle_was_pending)
- m_gjs->start_draining_job_queue();
+ m_gjs->start_draining_job_queue();
}
};
@@ -1275,6 +1270,8 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
{
JS::AutoSaveExceptionState saved_exc(m_cx);
ok = run_jobs_fallible() && ok;
+
+ stop_draining_job_queue();
}
auto_profile_exit(auto_profile);
@@ -1285,13 +1282,17 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
}
if (exit_status_p) {
+ uint8_t code;
if (retval.isInt32()) {
int code = retval.toInt32();
gjs_debug(GJS_DEBUG_CONTEXT,
"Script returned integer code %d", code);
*exit_status_p = code;
+ } else if (should_exit(&code)) {
+ *exit_status_p = code;
} else {
- /* Assume success if no integer was returned */
+ /* Assume success if no integer was returned and should exit isn't
+ * set */
*exit_status_p = 0;
}
}
@@ -1325,9 +1326,7 @@ bool GjsContextPrivate::eval_module(const char* identifier,
return false;
}
- bool ok = true;
- if (!JS::ModuleEvaluate(m_cx, obj))
- ok = false;
+ bool ok = JS::ModuleEvaluate(m_cx, obj);
/* The promise job queue should be drained even on error, to finish
* outstanding async tasks before the context is torn down. Drain after
@@ -1336,6 +1335,8 @@ bool GjsContextPrivate::eval_module(const char* identifier,
{
JS::AutoSaveExceptionState saved_exc(m_cx);
ok = run_jobs_fallible() && ok;
+
+ stop_draining_job_queue();
}
auto_profile_exit(auto_profile);
diff --git a/gjs/promise.cpp b/gjs/promise.cpp
new file mode 100644
index 00000000..7afb991a
--- /dev/null
+++ b/gjs/promise.cpp
@@ -0,0 +1,212 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#include <config.h>
+
+#include <js/CallArgs.h>
+#include <js/RootingAPI.h>
+#include <js/TypeDecls.h>
+#include <jsapi.h>
+
+#include "gjs/context-private.h"
+#include "gjs/promise.h"
+
+// G_PRIORITY_HIGH is -100, we set -1000 to ensure our source
+// always has the greatest priority. This means our prepare will
+// be called before other sources, and prepare will determine whether
+// we dispatch.
+#define GJS_PROMISE_JOB_QUEUE_SOURCE_PRIORITY -1000
+
+class GjsPromiseJobQueueSource;
+
+typedef gboolean (*GjsPromiseJobQueueSourceFunc)(void* promise_queue_source);
+
+/**
+ * A private class which holds the state for GjsPromiseJobQueueSource
+ * GSources and the GSourceFuncs for the source behavior.
+ */
+class GjsPromiseJobQueueSource {
+ public:
+ // The parent source.
+ GSource parent;
+ // The private GJS context this source runs within.
+ GjsContextPrivate* cx;
+ // The thread-default GMainContext
+ GMainContext* main_context;
+ GCancellable* cancellable;
+ int source_id;
+
+ private:
+ // Called to determine whether the source should run (dispatch) in the
+ // next event loop iteration. If the job queue is not empty we return true
+ // to schedule a dispatch, if the job queue has been empty we quit the main
+ // loop. This should return execution to gjs_spin_event_loop which may
+ // restart the loop if additional jobs are added.
+ static gboolean prepare(GSource* source, gint* timeout [[maybe_unused]]) {
+ auto promise_queue_source =
+ reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+ GjsContextPrivate* cx = promise_queue_source->cx;
+ if (!cx->empty())
+ return true;
+
+ g_main_context_wakeup(promise_queue_source->main_context);
+ return false;
+ }
+
+ // If the job queue is empty, dispatch will quit the event loop
+ // otherwise it will drain the job queue. Dispatch must always
+ // return G_SOURCE_CONTINUE, it should never remove the source
+ // from the loop.
+ static gboolean dispatch(GSource* source, GSourceFunc callback,
+ gpointer data [[maybe_unused]]) {
+ auto promise_queue_source =
+ reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+ GjsPromiseJobQueueSourceFunc func =
+ reinterpret_cast<GjsPromiseJobQueueSourceFunc>(callback);
+
+ // The ready time is sometimes set to 0 to kick us out of polling,
+ // we need to reset the value here or this source will always be the
+ // next one to execute. (it will starve the other sources)
+ g_source_set_ready_time(source, -1);
+
+ func(promise_queue_source);
+
+ return G_SOURCE_CONTINUE;
+ }
+
+ // Removes the GjsPrivateContext reference.
+ static void finalize(GSource* source) {
+ auto promise_queue_source =
+ reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+ promise_queue_source->cx = nullptr;
+
+ g_main_context_unref(promise_queue_source->main_context);
+ promise_queue_source->main_context = nullptr;
+ }
+
+ static gboolean callback(void* source) {
+ auto promise_queue_source =
+ reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+ if (g_cancellable_is_cancelled(promise_queue_source->cancellable))
+ return G_SOURCE_REMOVE;
+
+ GjsContextPrivate* cx = promise_queue_source->cx;
+ if (cx->empty()) {
+ g_main_context_wakeup(promise_queue_source->main_context);
+ }
+
+ // Drain the job queue.
+ cx->runJobs(cx->context());
+
+ return G_SOURCE_CONTINUE;
+ }
+
+ // g_source_new does not accept const values so
+ // this static member is defined outside of the
+ // class body.
+ static GSourceFuncs source_funcs;
+
+ public:
+ // Creates a new GSource with this class' state and source_funcs.
+ static GSource* create(GjsContextPrivate* cx, GCancellable* cancellable) {
+ g_return_val_if_fail(cx != nullptr, nullptr);
+ g_return_val_if_fail(
+ cancellable == nullptr || G_IS_CANCELLABLE(cancellable), nullptr);
+
+ GSource* source =
+ g_source_new(&source_funcs, sizeof(GjsPromiseJobQueueSource));
+ g_source_set_priority(source, GJS_PROMISE_JOB_QUEUE_SOURCE_PRIORITY);
+ g_source_set_callback(source, &callback, nullptr, nullptr);
+ g_source_set_name(source, "GjsPromiseJobQueueSource");
+
+ // TODO(ewlsh): Do we need this?
+ // g_source_set_can_recurse(source, true);
+ auto promise_queue_source =
+ reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+ promise_queue_source->cx = cx;
+ promise_queue_source->main_context =
+ g_main_context_ref_thread_default();
+ promise_queue_source->source_id = -1;
+ promise_queue_source->cancellable = cancellable;
+
+ g_assert(promise_queue_source->main_context);
+
+ // Add a cancellable source.
+ GSource* cancellable_source = g_cancellable_source_new(cancellable);
+ g_source_set_dummy_callback(cancellable_source);
+ g_source_add_child_source(source, cancellable_source);
+ g_source_unref(cancellable_source);
+
+ return source;
+ }
+};
+
+GSourceFuncs GjsPromiseJobQueueSource::source_funcs = {
+ &GjsPromiseJobQueueSource::prepare,
+ nullptr,
+ &GjsPromiseJobQueueSource::dispatch,
+ &GjsPromiseJobQueueSource::finalize,
+ nullptr,
+ nullptr,
+};
+
+/**
+ * gjs_promise_job_queue_source_new:
+ *
+ * @brief Creates a new GjsPromiseJobQueueSource GSource with an
+ * optional cancellable.
+ *
+ * @param cx the current JSContext
+ * @param cancellable an optional cancellable
+ *
+ * @returns the created source
+ */
+GSource* gjs_promise_job_queue_source_new(GjsContextPrivate* cx,
+ GCancellable* cancellable) {
+ return GjsPromiseJobQueueSource::create(cx, cancellable);
+}
+
+void gjs_promise_job_queue_source_attach(GSource* source) {
+ auto promise_queue_source =
+ reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+ promise_queue_source->source_id =
+ g_source_attach(source, promise_queue_source->main_context);
+}
+
+void gjs_promise_job_queue_source_remove(GSource* source) {
+ auto promise_queue_source =
+ reinterpret_cast<GjsPromiseJobQueueSource*>(source);
+
+ g_source_remove(promise_queue_source->source_id);
+ g_source_destroy(source);
+ g_source_unref(source);
+}
+
+void gjs_promise_job_queue_source_wakeup(GSource* source) {
+ g_source_set_ready_time(source, 0);
+}
+
+GJS_JSAPI_RETURN_CONVENTION
+static bool run_func(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ auto gjs = GjsContextPrivate::from_cx(cx);
+ gjs->runJobs(cx);
+
+ args.rval().setUndefined();
+ return true;
+}
+
+static JSFunctionSpec gjs_native_promise_module_funcs[] = {
+ JS_FN("run", run_func, 2, 0), JS_FS_END};
+
+bool gjs_define_native_promise_stuff(JSContext* cx,
+ JS::MutableHandleObject module) {
+ module.set(JS_NewPlainObject(cx));
+ return JS_DefineFunctions(cx, module, gjs_native_promise_module_funcs);
+}
diff --git a/gjs/promise.h b/gjs/promise.h
new file mode 100644
index 00000000..6608b3af
--- /dev/null
+++ b/gjs/promise.h
@@ -0,0 +1,31 @@
+/* -*- mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+ * SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+ */
+
+#ifndef GJS_PROMISE_H_
+#define GJS_PROMISE_H_
+
+#include <config.h>
+
+#include <gio/gio.h>
+#include <glib.h>
+
+#include <js/TypeDecls.h>
+
+#include "gjs/context-private.h"
+
+GSource* gjs_promise_job_queue_source_new(GjsContextPrivate* cx,
+ GCancellable* cancellable);
+
+void gjs_promise_job_queue_source_attach(GSource* source);
+
+void gjs_promise_job_queue_source_remove(GSource* source);
+
+void gjs_promise_job_queue_source_wakeup(GSource* source);
+
+bool gjs_define_native_promise_stuff(JSContext* cx,
+ JS::MutableHandleObject module);
+
+#endif // GJS_PROMISE_H_
diff --git a/meson.build b/meson.build
index b6f6b40e..bbfee34b 100644
--- a/meson.build
+++ b/meson.build
@@ -407,6 +407,7 @@ libgjs_sources = [
'gjs/objectbox.cpp', 'gjs/objectbox.h',
'gjs/profiler.cpp', 'gjs/profiler-private.h',
'gjs/text-encoding.cpp', 'gjs/text-encoding.h',
+ 'gjs/promise.cpp', 'gjs/promise.h',
'gjs/stack.cpp',
'modules/console.cpp', 'modules/console.h',
'modules/modules.cpp', 'modules/modules.h',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]