[gjs/ewlsh/implicit-mainloop: 1/3] Implement custom GSource to handle promise queueing




commit 1c4fd97348b2658050fac825abf089129b320532
Author: Evan Welsh <contact evanwelsh com>
Date:   Sat Sep 4 19:58:31 2021 -0700

    Implement custom GSource to handle promise queueing
    
    (Cleanups and refactoring from Marco and Philip)
    
    Closes #1

 doc/Custom-GSources.md |  14 ++++
 gjs/context-private.h  |   4 +-
 gjs/context.cpp        |  51 +++++---------
 gjs/promise.cpp        | 188 +++++++++++++++++++++++++++++++++++++++++++++++++
 gjs/promise.h          |  64 +++++++++++++++++
 meson.build            |   1 +
 tools/process_iwyu.py  |   2 +
 tools/run_iwyu.sh      |   2 +-
 8 files changed, 289 insertions(+), 37 deletions(-)
---
diff --git a/doc/Custom-GSources.md b/doc/Custom-GSources.md
new file mode 100644
index 00000000..4fe1adc5
--- /dev/null
+++ b/doc/Custom-GSources.md
@@ -0,0 +1,14 @@
+## Custom GSources
+
+GLib allows custom GSources to be added to the main loop. A custom GSource can control under what conditions
+it is dispatched. You can read more about GLib's main loop [here][glib-mainloop-docs].
+
+Within GJS, we have implemented a custom GSource to handle Promise execution. It dispatches whenever a 
Promise
+is queued, occurring before any other GLib events. This mimics the behavior of a [microtask 
queue](mdn-microtasks) in other JavaScript environments. You can read an introduction to building custom 
GSources within the archived developer documentation [here][custom-gsource-tutorial] or, if unavailable, via 
[the original source code][custom-gsource-tutorial-source]. Another great resource is Philip Withnall's ["A 
detailed look at GSource"][gsource-blog-post]<sup>[[permalink]][gsource-blog-post-archive]</sup>.
+
+[gsource-blog-post]: https://tecnocode.co.uk/2015/05/05/a-detailed-look-at-gsource/
+[gsource-blog-post-archive]: 
https://web.archive.org/web/20201013000618/https://tecnocode.co.uk/2015/05/05/a-detailed-look-at-gsource/
+[mdn-microtasks]: https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
+[glib-mainloop-docs]: https://docs.gtk.org/glib/main-loop.html#creating-new-source-types
+[custom-gsource-tutorial]: 
https://developer-old.gnome.org/gnome-devel-demos/unstable/custom-gsource.c.html.en
+[custom-gsource-tutorial-source]: 
https://gitlab.gnome.org/Archive/gnome-devel-docs/-/blob/703816cec292293fd337b6db8520b9b0afa7b3c9/platform-demos/C/custom-gsource.c.page
\ No newline at end of file
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 75b04bf0..b8dffba2 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -37,6 +37,7 @@
 #include "gjs/jsapi-util.h"
 #include "gjs/macros.h"
 #include "gjs/profiler.h"
+#include "gjs/promise.h"
 
 namespace js {
 class SystemAllocPolicy;
@@ -78,7 +79,7 @@ class GjsContextPrivate : public JS::JobQueue {
     std::vector<std::string> m_args;
 
     JobQueueStorage m_job_queue;
-    unsigned m_idle_drain_handler;
+    Gjs::PromiseJobDispatcher m_dispatcher;
 
     std::vector<std::pair<DestroyNotify, void*>> m_destroy_notifications;
     std::vector<Gjs::Closure::Ptr> m_async_closures;
@@ -134,7 +135,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 734ac68d..594cf4d9 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"
@@ -324,6 +325,8 @@ gjs_context_class_init(GjsContextClass *klass)
         g_irepository_prepend_search_path(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);
@@ -405,6 +408,8 @@ void GjsContextPrivate::unregister_notifier(DestroyNotify notify_func,
 
 void GjsContextPrivate::dispose(void) {
     if (m_cx) {
+        stop_draining_job_queue();
+
         gjs_debug(GJS_DEBUG_CONTEXT,
                   "Notifying reference holders of GjsContext dispose");
 
@@ -532,8 +537,8 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
     : m_public_context(public_context),
       m_cx(cx),
       m_owner_thread(std::this_thread::get_id()),
+      m_dispatcher(this),
       m_environment_preparer(cx) {
-
     JS_SetGCCallback(
         cx,
         [](JSContext*, JSGCStatus status, JS::GCReason reason, void* data) {
@@ -654,6 +659,8 @@ GjsContextPrivate::GjsContextPrivate(JSContext* cx, GjsContext* public_context)
             cx, "resource:///org/gnome/gjs/modules/esm/_bootstrap/default.js",
             "ESM bootstrap");
     }
+
+    start_draining_job_queue();
 }
 
 void GjsContextPrivate::set_args(std::vector<std::string>&& args) {
@@ -895,32 +902,15 @@ bool GjsContextPrivate::should_exit(uint8_t* exit_code_p) const {
 }
 
 void GjsContextPrivate::start_draining_job_queue(void) {
-    if (!m_idle_drain_handler) {
-        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);
-    }
+    gjs_debug(GJS_DEBUG_CONTEXT, "Starting promise job dispatcher");
+    m_dispatcher.start();
 }
 
 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;
-    }
-}
 
-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;
+    gjs_debug(GJS_DEBUG_CONTEXT, "Stopping promise job dispatcher");
+    m_dispatcher.stop();
 }
 
 JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) {
@@ -943,18 +933,13 @@ 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;
     }
 
     JS::JobQueueMayNotBeEmpty(m_cx);
-    start_draining_job_queue();
+    m_dispatcher.start();
     return true;
 }
 
@@ -1032,8 +1017,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;
 }
@@ -1042,14 +1027,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();
@@ -1059,8 +1042,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();
     }
 };
 
@@ -1292,7 +1274,8 @@ bool GjsContextPrivate::eval(const char* script, ssize_t script_len,
                       "Script returned integer code %d", 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;
         }
     }
diff --git a/gjs/promise.cpp b/gjs/promise.cpp
new file mode 100644
index 00000000..5123445b
--- /dev/null
+++ b/gjs/promise.cpp
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+// SPDX-FileCopyrightText: 2021 Marco Trevisan <mail 3v1n0 net>
+
+#include <config.h>
+
+#include <stddef.h>  // for size_t
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+#include <js/CallArgs.h>
+#include <js/PropertySpec.h>
+#include <js/RootingAPI.h>
+#include <jsapi.h>
+#include "gjs/macros.h"
+#include <js/TypeDecls.h>
+
+#include "gjs/context-private.h"
+#include "gjs/jsapi-util.h"
+#include "gjs/promise.h"
+
+/**
+ * promise.cpp - This file implements a custom GSource, PromiseJobQueueSource,
+ * which handles promise dispatching within GJS. Custom GSources are able to
+ * control under which conditions they dispatch. PromiseJobQueueSource will
+ * always dispatch if even a single Promise is enqueued and will continue
+ * dispatching until all Promises (also known as "Jobs" within SpiderMonkey)
+ * are run. While this does technically mean Promises can starve the mainloop
+ * if run recursively, this is intentional. Within JavaScript Promises are
+ * considered "microtasks" and a microtask must run before any other task
+ * continues.
+ *
+ * PromiseJobQueueSource is attached to the thread's default GMainContext with
+ * a priority of -1000. This is 10x the priority of G_PRIORITY_HIGH and no
+ * application code should attempt to override this.
+ *
+ * See doc/Custom-GSources.md for more background information on custom
+ * GSources and microtasks
+ */
+
+namespace Gjs {
+
+/**
+ * @brief a custom GSource which handles draining our job queue.
+ */
+class PromiseJobDispatcher::Source : public GSource {
+    // The private GJS context this source runs within.
+    GjsContextPrivate* m_gjs;
+    // The main context this source attaches to.
+    GjsAutoMainContext m_main_context;
+    // The cancellable that stops this source.
+    GjsAutoUnref<GCancellable> m_cancellable;
+
+    // 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.
+    static constexpr int PRIORITY = -1000;
+
+    // GSource custom functions
+    static GSourceFuncs source_funcs;
+
+    // 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.
+    gboolean prepare(int* timeout [[maybe_unused]]) { return !m_gjs->empty(); }
+
+    gboolean dispatch() {
+        if (g_cancellable_is_cancelled(m_cancellable))
+            return G_SOURCE_REMOVE;
+
+        // 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(this, -1);
+
+        // Drain the job queue.
+        m_gjs->runJobs(m_gjs->context());
+
+        return G_SOURCE_CONTINUE;
+    }
+
+ public:
+    /**
+     * @brief Constructs a new GjsPromiseJobQueueSource GSource and adds a
+     * reference to the associated main context.
+     *
+     * @param cx the current JSContext
+     * @param cancellable an optional cancellable
+     */
+    Source(GjsContextPrivate* gjs, GMainContext* main_context)
+        : m_gjs(gjs),
+          m_main_context(main_context, GjsAutoTakeOwnership()),
+          m_cancellable(g_cancellable_new()) {
+        g_source_set_priority(this, PRIORITY);
+#if GLIB_CHECK_VERSION(2, 70, 0)
+        g_source_set_static_name(this, "GjsPromiseJobQueueSource");
+#else
+        g_source_set_name(this, "GjsPromiseJobQueueSource");
+#endif
+
+        // Add our cancellable source to our main source,
+        // this will trigger the main source if our cancellable
+        // is cancelled.
+        g_source_add_child_source(this, m_cancellable_source);
+        // Our cancellable source has no action, so set a dummy
+        // callback.
+        g_source_set_dummy_callback(m_cancellable_source);
+    }
+
+    void* operator new(size_t size) {
+        return g_source_new(&source_funcs, size);
+    }
+    void operator delete(void* p) { g_source_unref(static_cast<GSource*>(p)); }
+
+    /**
+     * @brief Trigger the cancellable, detaching our source.
+     */
+    void cancel() { g_cancellable_cancel(m_cancellable); }
+    /**
+     * @brief Reset the cancellable and prevent the source from
+     * stopping, overriding a previous cancel() call. Called by
+     * start() in PromiseQueueJobDispatcher to ensure the
+     * custom source will start.
+     */
+    void reset() { g_cancellable_reset(m_cancellable); }
+};
+
+GSourceFuncs PromiseJobDispatcher::Source::source_funcs = {
+    [](GSource* source, int* timeout) {
+        return static_cast<Source*>(source)->prepare(timeout);
+    },
+    nullptr,  // check
+    [](GSource* source, GSourceFunc, void*) {
+        return static_cast<Source*>(source)->dispatch();
+    },
+    [](GSource* source) { static_cast<Source*>(source)->~Source(); },
+};
+
+PromiseJobDispatcher::PromiseJobDispatcher(GjsContextPrivate* gjs)
+    // Acquire a guaranteed reference to this thread's default main context
+    : m_main_context(g_main_context_ref_thread_default()),
+      // Create and reference our custom GSource
+      m_source(std::make_unique<Source>(gjs, m_main_context)) {}
+
+PromiseJobDispatcher::~PromiseJobDispatcher() {
+    g_source_destroy(m_source.get());
+}
+
+bool PromiseJobDispatcher::is_running() {
+    return !!g_source_get_context(m_source.get());
+}
+
+void PromiseJobDispatcher::start() {
+    // Reset the cancellable
+    m_source->reset();
+
+    // Don't re-attach if the task is already running
+    if (is_running())
+        return;
+
+    g_source_attach(m_source.get(), m_main_context);
+}
+
+void PromiseJobDispatcher::stop() { m_source->cancel(); }
+
+};  // namespace Gjs
+
+GJS_JSAPI_RETURN_CONVENTION
+bool drain_microtask_queue_func(JSContext* cx, unsigned argc, JS::Value* vp) {
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+    GjsContextPrivate* gjs = GjsContextPrivate::from_cx(cx);
+    gjs->runJobs(cx);
+
+    args.rval().setUndefined();
+    return true;
+}
+
+JSFunctionSpec gjs_native_promise_module_funcs[] = {
+    JS_FN("drainMicrotaskQueue", drain_microtask_queue_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..91471968
--- /dev/null
+++ b/gjs/promise.h
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021 Evan Welsh <contact evanwelsh com>
+
+#pragma once
+
+#include <config.h>
+
+#include <memory>
+
+#include <glib.h>
+
+#include <js/TypeDecls.h>
+
+#include "gjs/jsapi-util.h"
+
+class GjsContextPrivate;
+
+using GjsAutoMainContext =
+    GjsAutoPointer<GMainContext, GMainContext, g_main_context_unref,
+                   g_main_context_ref>;
+
+namespace Gjs {
+
+/**
+ * @brief A class which wraps a custom GSource and handles associating it with a
+ * GMainContext. While it is running, it will attach the source to the main
+ * context so that promise jobs are run at the appropriate time.
+ */
+class PromiseJobDispatcher {
+    class Source;
+    // The thread-default GMainContext
+    GjsAutoMainContext m_main_context;
+    // The custom source.
+    std::unique_ptr<Source> m_source;
+
+ public:
+    /**
+     * @brief Constructs a new PromiseJobQueueDispatcher
+     *
+     * @param cx the current private context
+     */
+    explicit PromiseJobDispatcher(GjsContextPrivate*);
+    ~PromiseJobDispatcher();
+
+    /**
+     * @brief Start (or resume) dispatching jobs from the promise job queue
+     */
+    void start();
+
+    /**
+     * @brief Stop dispatching
+     */
+    void stop();
+
+    /**
+     * @brief Whether the dispatcher is currently running
+     */
+    bool is_running();
+};
+
+};  // namespace Gjs
+
+bool gjs_define_native_promise_stuff(JSContext* cx,
+                                     JS::MutableHandleObject module);
diff --git a/meson.build b/meson.build
index 78bfc8a6..437b3fd3 100644
--- a/meson.build
+++ b/meson.build
@@ -424,6 +424,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',
diff --git a/tools/process_iwyu.py b/tools/process_iwyu.py
index 718349b2..2cc32e62 100755
--- a/tools/process_iwyu.py
+++ b/tools/process_iwyu.py
@@ -106,6 +106,8 @@ FALSE_POSITIVES = (
      'for remove_reference<>::type'),
     ('gjs/profiler.cpp', '#include <type_traits>',
      'for remove_reference<>::type'),
+    ('gjs/promise.cpp', '#include <type_traits>',
+     'for remove_reference<>::type'),
     ('test/gjs-test-jsapi-utils.cpp', '#include <type_traits>',
      'for remove_reference<>::type'),
 
diff --git a/tools/run_iwyu.sh b/tools/run_iwyu.sh
index 319da33e..7e1db246 100755
--- a/tools/run_iwyu.sh
+++ b/tools/run_iwyu.sh
@@ -71,7 +71,7 @@ for FILE in $SRCDIR/gi/*.cpp $SRCDIR/gjs/atoms.cpp $SRCDIR/gjs/byteArray.cpp \
     $SRCDIR/gjs/deprecation.cpp $SRCDIR/gjs/error-types.cpp \
     $SRCDIR/gjs/engine.cpp $SRCDIR/gjs/global.cpp $SRCDIR/gjs/importer.cpp \
     $SRCDIR/gjs/jsapi-util*.cpp $SRCDIR/gjs/module.cpp $SRCDIR/gjs/native.cpp \
-    $SRCDIR/gjs/objectbox.cpp $SRCDIR/gjs/stack.cpp \
+    $SRCDIR/gjs/objectbox.cpp $SRCDIR/gjs/promise.cpp $SRCDIR/gjs/stack.cpp \
     $SRCDIR/modules/cairo-*.cpp $SRCDIR/modules/console.cpp \
     $SRCDIR/modules/print.cpp $SRCDIR/modules/system.cpp $SRCDIR/test/*.cpp \
     $SRCDIR/util/*.cpp $SRCDIR/libgjs-private/*.c


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]