diff options
| author | Evan Welsh <contact@evanwelsh.com> | 2021-09-04 19:58:31 -0700 |
|---|---|---|
| committer | Evan Welsh <contact@evanwelsh.com> | 2021-09-10 21:03:36 -0700 |
| commit | 374d4d1eeccf108666d6519cf6c4f63f7dc729ec (patch) | |
| tree | 5583a1a5e1cf55b1d8f4aecc5057a3fb20397aaf | |
| parent | 0703c82f5c5834954ca7115d89c0c7f265fc81b2 (diff) | |
| download | gjs-ewlsh/implicit-mainloop.tar.gz | |
Implement custom GSource to handle promise queueingewlsh/implicit-mainloop
| -rw-r--r-- | gjs/context-private.h | 4 | ||||
| -rw-r--r-- | gjs/context.cpp | 60 | ||||
| -rw-r--r-- | gjs/promise.cpp | 153 | ||||
| -rw-r--r-- | gjs/promise.h | 120 | ||||
| -rw-r--r-- | meson.build | 1 |
5 files changed, 298 insertions, 40 deletions
diff --git a/gjs/context-private.h b/gjs/context-private.h index 75b04bf0..776b919e 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::PromiseJobQueueTask m_promise_queue_task; 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 d8992a58..d58de59e 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); @@ -404,6 +407,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"); @@ -531,8 +536,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_promise_queue_task(this), m_environment_preparer(cx) { - JS_SetGCCallback( cx, [](JSContext*, JSGCStatus status, JS::GCReason reason, void* data) { @@ -653,6 +658,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) { @@ -894,32 +901,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 queue task"); + m_promise_queue_task.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 queue task"); + m_promise_queue_task.stop(); } JSObject* GjsContextPrivate::getIncumbentGlobal(JSContext* cx) { @@ -942,18 +932,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_promise_queue_task.start(); return true; } @@ -1031,8 +1016,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 +1026,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 +1041,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(); } }; @@ -1285,13 +1267,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 +1311,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 diff --git a/gjs/promise.cpp b/gjs/promise.cpp new file mode 100644 index 00000000..f650f820 --- /dev/null +++ b/gjs/promise.cpp @@ -0,0 +1,153 @@ +// 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 + +namespace detail { +typedef gboolean (*PromiseJobQueueSourceFunc)(void* promise_queue_source); + +// 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 PromiseJobQueueSource::prepare(GSource* source, + int* timeout [[maybe_unused]]) { + auto promise_queue_source = + reinterpret_cast<PromiseJobQueueSource*>(source); + + GjsContextPrivate* cx = promise_queue_source->cx; + + return !cx->empty(); +} + +gboolean PromiseJobQueueSource::dispatch(GSource* source, GSourceFunc callback, + void* data [[maybe_unused]]) { + auto promise_queue_source = + reinterpret_cast<PromiseJobQueueSource*>(source); + if (g_cancellable_is_cancelled(promise_queue_source->cancellable)) + return G_SOURCE_REMOVE; + + PromiseJobQueueSourceFunc func = + reinterpret_cast<PromiseJobQueueSourceFunc>(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. +void PromiseJobQueueSource::finalize(GSource* source) { + reinterpret_cast<PromiseJobQueueSource*>(source)->~PromiseJobQueueSource(); +} + +gboolean PromiseJobQueueSource::callback(void* source) { + auto promise_queue_source = + reinterpret_cast<PromiseJobQueueSource*>(source); + if (g_cancellable_is_cancelled(promise_queue_source->cancellable)) + return G_SOURCE_REMOVE; + + GjsContextPrivate* cx = promise_queue_source->cx; + + // Drain the job queue. + cx->runJobs(cx->context()); + + return G_SOURCE_CONTINUE; +} + +PromiseJobQueueSource* PromiseJobQueueSource::create( + GjsContextPrivate* cx, GMainContext* main_context) { + PromiseJobQueueSource* source = reinterpret_cast<PromiseJobQueueSource*>( + g_source_new(&source_funcs, sizeof(PromiseJobQueueSource))); + 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"); + + source->cx = cx; + source->main_context = g_main_context_ref(main_context); + source->cancellable = g_cancellable_new(); + + GSource* cancellable_source = g_cancellable_source_new(source->cancellable); + g_source_add_child_source(source, cancellable_source); + g_source_set_dummy_callback(cancellable_source); + + return source; +} + +void PromiseJobQueueSource::cancel() { g_cancellable_cancel(cancellable); } + +void PromiseJobQueueSource::reset() { g_cancellable_reset(cancellable); } + +PromiseJobQueueSource* PromiseJobQueueSource::ref() { + return reinterpret_cast<PromiseJobQueueSource*>(g_source_ref(this)); +} +void PromiseJobQueueSource::unref() { g_source_unref(this); } + +GSourceFuncs PromiseJobQueueSource::source_funcs = { + &PromiseJobQueueSource::prepare, nullptr, &PromiseJobQueueSource::dispatch, + &PromiseJobQueueSource::finalize, nullptr, nullptr, +}; + +} // namespace detail + +namespace Gjs { + +PromiseJobQueueTask::PromiseJobQueueTask(GjsContextPrivate* cx) + // Acquire a guaranteed reference to this thread's default main context + : main_context(g_main_context_ref_thread_default()), + // Create and reference our custom GSource + source(detail::PromiseJobQueueSource::create(cx, main_context)->ref()) {} + +bool PromiseJobQueueTask::isRunning() { return !!g_source_get_context(source); } + +void PromiseJobQueueTask::start() { + // Reset the cancellable + source->reset(); + + // Don't re-attach if the task is already running + if (isRunning()) + return; + + g_source_attach(source, main_context); +} + +void PromiseJobQueueTask::stop() { source->cancel(); } + +}; // namespace Gjs + +GJS_JSAPI_RETURN_CONVENTION +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; +} + +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..4e3e2bba --- /dev/null +++ b/gjs/promise.h @@ -0,0 +1,120 @@ +/* + * 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> + +class GjsContextPrivate; + +namespace detail { +/** + * @brief a custom GSource which handles draining our job queue. + */ +class PromiseJobQueueSource : public GSource { + // The private GJS context this source runs within. + GjsContextPrivate* cx; + // The main context this source attaches to. + GMainContext* main_context; + // The cancellable that stops this source. + GjsAutoPointer<GCancellable, void, g_free> cancellable; + + // GSource custom functions + + static gboolean prepare(GSource* source, gint* timeout [[maybe_unused]]); + + static gboolean dispatch(GSource* source, GSourceFunc callback, + gpointer data [[maybe_unused]]); + + static void finalize(GSource* source); + + static gboolean callback(void* source); + + static GSourceFuncs source_funcs; + + public: + /** + * create: + * + * @brief Creates a new GjsPromiseJobQueueSource GSource and adds a + * reference to the associated main context. + * + * @param cx the current JSContext + * @param cancellable an optional cancellable + * + * @returns the created source + */ + static PromiseJobQueueSource* create(GjsContextPrivate* cx, + GMainContext* main_context); + + /** + * @brief Trigger the cancellable, detaching our source. + */ + void cancel(); + /** + * @brief Reset the cancellable + */ + void reset(); + /** + * Add a reference to the source + */ + PromiseJobQueueSource* ref(); + /** + * Drop a reference to the source + */ + void unref(); +}; +}; // namespace detail + +namespace Gjs { + +/** + * @brief A class which wraps a custom GSource and handles + * associating it with a main context. All memory is handled, + * so this class is safe to use without raw pointers. + */ +class PromiseJobQueueTask { + // The thread-default GMainContext + GjsAutoPointer<GMainContext, GMainContext, g_main_context_unref> + main_context; + // The custom source. + GjsAutoPointer<detail::PromiseJobQueueSource, GSource, g_source_unref> + source; + + public: + /** + * @brief Constructs a new GjsPromiseJobQueue + * + * @param cx the current private context + */ + explicit PromiseJobQueueTask(GjsContextPrivate* cx); + + /** + * @brief Start (or restart) the task + */ + void start(); + + /** + * @brief Stop the task + */ + void stop(); + + /** + * @brief whether the task is currently running in the associated context + */ + bool isRunning(); +}; +}; // namespace Gjs + +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', |
