// SPDX-License-Identifier: MIT OR LGPL-2.0-or-later // SPDX-FileCopyrightText: 2021 Evan Welsh // SPDX-FileCopyrightText: 2021 Marco Trevisan #include #include // for size_t #include #include #include #include // for JS::IsCallable #include #include // for JS_DefineFunctions #include #include #include #include // for JS_NewPlainObject #include "gjs/context-private.h" #include "gjs/jsapi-util-args.h" #include "gjs/jsapi-util.h" #include "gjs/macros.h" #include "gjs/promise.h" #include "util/log.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 default 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 m_cancellable; GjsAutoPointer m_cancellable_source; // G_PRIORITY_HIGH is normally -100, we set 10 times that 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 = 10 * G_PRIORITY_HIGH; // 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()), m_cancellable_source(g_cancellable_source_new(m_cancellable)) { 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); } void* operator new(size_t size) { return g_source_new(&source_funcs, size); } void operator delete(void* p) { g_source_unref(static_cast(p)); } bool is_running() { return !!g_source_get_context(this); } /** * @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 * PromiseJobDispatcher to ensure the custom source will start. */ void reset() { if (!g_cancellable_is_cancelled(m_cancellable)) return; gjs_debug(GJS_DEBUG_MAINLOOP, "Uncancelling promise job dispatcher"); if (is_running()) g_source_remove_child_source(this, m_cancellable_source); else g_source_destroy(m_cancellable_source); // Drop the old cancellable and create a new one, as per // https://docs.gtk.org/gio/method.Cancellable.reset.html m_cancellable = g_cancellable_new(); m_cancellable_source = g_cancellable_source_new(m_cancellable); g_source_add_child_source(this, m_cancellable_source); } }; GSourceFuncs PromiseJobDispatcher::Source::source_funcs = { [](GSource* source, int* timeout) { return static_cast(source)->prepare(timeout); }, nullptr, // check [](GSource* source, GSourceFunc, void*) { return static_cast(source)->dispatch(); }, [](GSource* source) { static_cast(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(gjs, m_main_context)) {} PromiseJobDispatcher::~PromiseJobDispatcher() { g_source_destroy(m_source.get()); } bool PromiseJobDispatcher::is_running() { return m_source->is_running(); } void PromiseJobDispatcher::start() { // Reset the cancellable m_source->reset(); // Don't re-attach if the task is already running if (is_running()) return; gjs_debug(GJS_DEBUG_MAINLOOP, "Starting promise job dispatcher"); g_source_attach(m_source.get(), m_main_context); } void PromiseJobDispatcher::stop() { gjs_debug(GJS_DEBUG_MAINLOOP, "Stopping promise job dispatcher"); m_source->cancel(); } }; // namespace Gjs GJS_JSAPI_RETURN_CONVENTION bool drain_microtask_queue(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; } GJS_JSAPI_RETURN_CONVENTION bool set_main_loop_hook(JSContext* cx, unsigned argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); JS::RootedObject callback(cx); if (!gjs_parse_call_args(cx, "setMainLoopHook", args, "o", "callback", &callback)) { return false; } if (!JS::IsCallable(callback)) { gjs_throw(cx, "Main loop hook must be callable"); return false; } gjs_debug(GJS_DEBUG_MAINLOOP, "Set main loop hook to %s", gjs_debug_object(callback).c_str()); GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx); if (!priv->set_main_loop_hook(callback)) { gjs_throw( cx, "A mainloop is already running. Did you already call runAsync()?"); return false; } args.rval().setUndefined(); return true; } JSFunctionSpec gjs_native_promise_module_funcs[] = { JS_FN("drainMicrotaskQueue", &drain_microtask_queue, 0, 0), JS_FN("setMainLoopHook", &set_main_loop_hook, 1, 0), JS_FS_END}; bool gjs_define_native_promise_stuff(JSContext* cx, JS::MutableHandleObject module) { module.set(JS_NewPlainObject(cx)); if (!module) return false; return JS_DefineFunctions(cx, module, gjs_native_promise_module_funcs); }