[gjs/ewlsh/main-loop-hooks] Introduce runAsync() to run mainloops without blocking module resolution
- From: Evan Welsh <ewlsh src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/ewlsh/main-loop-hooks] Introduce runAsync() to run mainloops without blocking module resolution
- Date: Mon, 28 Feb 2022 05:09:32 +0000 (UTC)
commit 35e0298f73c687f43d8e0c1b44c7b0846f41df9b
Author: Evan Welsh <contact evanwelsh com>
Date: Sun Feb 27 20:16:19 2022 -0800
Introduce runAsync() to run mainloops without blocking module resolution
With top-level await all modules are now promises, if Gtk.Application.run()
or GLib.MainLoop.run() is called within a module it will block
all other promises as run() is a synchronous, blocking function.
To work around this there is now a setMainLoopHook function exposed
which runs a callback after module resolution is complete. This allows
APIs to "install" a mainloop asynchronously.
For Gio.Application, Gtk.Application, and GLib.MainLoop there are now
runAsync() versions of their run() functions. runAsync() returns a
Promise which will resolve once the mainloop has exited.
Fixes #468
gjs/context-private.h | 16 ++++++++
gjs/context.cpp | 88 +++++++++++++++++++++++++++++++++++++-----
gjs/mainloop.cpp | 7 ++--
gjs/promise.cpp | 28 +++++++++++++-
js.gresource.xml | 1 +
modules/core/overrides/GLib.js | 72 ++++++++++++++++++++--------------
modules/core/overrides/Gio.js | 14 +++++++
modules/core/overrides/Gtk.js | 14 +++++++
modules/esm/mainloop.js | 44 +++++++++++++++++++++
9 files changed, 241 insertions(+), 43 deletions(-)
---
diff --git a/gjs/context-private.h b/gjs/context-private.h
index 3bc7d50fa..25e4fd1e0 100644
--- a/gjs/context-private.h
+++ b/gjs/context-private.h
@@ -59,12 +59,15 @@ using GTypeTable =
js::SystemAllocPolicy>;
class GjsContextPrivate : public JS::JobQueue {
+ friend Gjs::MainLoop;
+
public:
using DestroyNotify = void (*)(JSContext*, void* data);
private:
GjsContext* m_public_context;
JSContext* m_cx;
+ JS::Heap<JSFunction*> m_main_loop_hook;
JS::Heap<JSObject*> m_global;
JS::Heap<JSObject*> m_internal_global;
std::thread::id m_owner_thread;
@@ -176,6 +179,19 @@ class GjsContextPrivate : public JS::JobQueue {
[[nodiscard]] GjsContext* public_context() const {
return m_public_context;
}
+ [[nodiscard]] bool set_main_loop_hook(JSFunction* callback) {
+ if (!callback) {
+ m_main_loop_hook = callback;
+ return true;
+ }
+
+ if (m_main_loop_hook)
+ return false;
+
+ m_main_loop_hook = callback;
+ return true;
+ }
+ GJS_JSAPI_RETURN_CONVENTION bool run_main_loop_hook();
[[nodiscard]] JSContext* context() const { return m_cx; }
[[nodiscard]] JSObject* global() const { return m_global.get(); }
[[nodiscard]] JSObject* internal_global() const {
diff --git a/gjs/context.cpp b/gjs/context.cpp
index b1cb934bc..0d3783bad 100644
--- a/gjs/context.cpp
+++ b/gjs/context.cpp
@@ -349,6 +349,8 @@ void GjsContextPrivate::trace(JSTracer* trc, void* data) {
JS::TraceEdge<JSObject*>(trc, &gjs->m_global, "GJS global object");
JS::TraceEdge<JSObject*>(trc, &gjs->m_internal_global,
"GJS internal global object");
+ JS::TraceEdge<JSFunction*>(trc, &gjs->m_main_loop_hook,
+ "GJS main loop hook");
gjs->m_atoms->trace(trc);
gjs->m_job_queue.trace(trc);
gjs->m_object_init_list.trace(trc);
@@ -460,6 +462,7 @@ void GjsContextPrivate::dispose(void) {
JS_RemoveExtraGCRootsTracer(m_cx, &GjsContextPrivate::trace, this);
m_global = nullptr;
m_internal_global = nullptr;
+ m_main_loop_hook = nullptr;
gjs_debug(GJS_DEBUG_CONTEXT, "Freeing allocated resources");
delete m_fundamental_table;
@@ -1365,16 +1368,44 @@ bool GjsContextPrivate::eval(const char* script, size_t script_len,
JS::RootedValue retval(m_cx);
bool ok = eval_with_scope(nullptr, script, script_len, filename, &retval);
+ /**
+ * If there are no errors and the mainloop hook
+ * is set, cal it.
+ */
+ if (ok && m_main_loop_hook)
+ ok = run_main_loop_hook();
+
+ /**
+ * Spin the internal loop until the main loop hook
+ * is set or no holds remain.
+ */
+ if (ok) {
+ /**
+ * If the main loop returns false we cannot guarantee the state
+ * of our promise queue (a module promise could be pending) so
+ * instead of draining the queue we instead just exit.
+ */
+ ok = m_main_loop.spin(this);
+ }
+
+ // If the hook has been set again, enter a loop
+ // until an error is encountered or the main loop
+ // is quit.
+ while (ok && m_main_loop_hook) {
+ ok = run_main_loop_hook();
+
+ // Additional jobs could have been enqueued from the
+ // main loop hook
+ if (ok) {
+ ok = m_main_loop.spin(this);
+ }
+ }
/* The promise job queue should be drained even on error, to finish
* outstanding async tasks before the context is torn down. Drain after
* uncaught exceptions have been reported since draining runs callbacks.
- *
- * If the main loop returns false we cannot guarantee the state
- * of our promise queue (a module promise could be pending) so
- * instead of draining the queue we instead just exit.
*/
- if (!ok || m_main_loop.spin(this)) {
+ if (!ok) {
JS::AutoSaveExceptionState saved_exc(m_cx);
ok = run_jobs_fallible() && ok;
}
@@ -1398,6 +1429,14 @@ bool GjsContextPrivate::eval(const char* script, size_t script_len,
return ok;
}
+bool GjsContextPrivate::run_main_loop_hook() {
+ JS::RootedFunction hook(m_cx, m_main_loop_hook.get());
+ m_main_loop_hook = nullptr;
+ JS::RootedValue ignored_rval(m_cx);
+ return JS_CallFunction(m_cx, nullptr, hook, JS::HandleValueArray::empty(),
+ &ignored_rval);
+}
+
bool GjsContextPrivate::eval_module(const char* identifier,
uint8_t* exit_status_p, GError** error) {
AutoResetExit reset(this);
@@ -1439,15 +1478,44 @@ bool GjsContextPrivate::eval_module(const char* identifier,
on_context_module_rejected_log_exception, "context");
}
+ /**
+ * If there are no errors and the mainloop hook
+ * is set, cal it.
+ */
+ if (ok && m_main_loop_hook)
+ ok = run_main_loop_hook();
+
+ /**
+ * Spin the internal loop until the main loop hook
+ * is set or no holds remain.
+ */
+ if (ok) {
+ /**
+ * If the main loop returns false we cannot guarantee the state
+ * of our promise queue (a module promise could be pending) so
+ * instead of draining the queue we instead just exit.
+ */
+ ok = m_main_loop.spin(this);
+ }
+
+ // If the hook has been set again, enter a loop
+ // until an error is encountered or the main loop
+ // is quit.
+ while (ok && m_main_loop_hook) {
+ ok = run_main_loop_hook();
+
+ // Additional jobs could have been enqueued from the
+ // main loop hook
+ if (ok) {
+ ok = m_main_loop.spin(this);
+ }
+ }
+
/* The promise job queue should be drained even on error, to finish
* outstanding async tasks before the context is torn down. Drain after
* uncaught exceptions have been reported since draining runs callbacks.
- *
- * If the main loop returns false we cannot guarantee the state
- * of our promise queue (a module promise could be pending) so
- * instead of draining the queue we instead just exit.
*/
- if (!ok || m_main_loop.spin(this)) {
+ if (!ok) {
JS::AutoSaveExceptionState saved_exc(m_cx);
ok = run_jobs_fallible() && ok;
}
diff --git a/gjs/mainloop.cpp b/gjs/mainloop.cpp
index 91109d61a..bb1e3ade6 100644
--- a/gjs/mainloop.cpp
+++ b/gjs/mainloop.cpp
@@ -40,9 +40,10 @@ bool MainLoop::spin(GjsContextPrivate* gjs) {
return false;
}
} while (
- // and there are pending sources or the job queue is not empty
- // continue spinning the event loop.
- (can_block() || !gjs->empty()));
+ // and there is not a pending main loop hook
+ !gjs->m_main_loop_hook &&
+ // and there are pending sources continue spinning the event loop.
+ can_block());
return true;
}
diff --git a/gjs/promise.cpp b/gjs/promise.cpp
index 0ce4bd7c5..9a26d3aed 100644
--- a/gjs/promise.cpp
+++ b/gjs/promise.cpp
@@ -16,6 +16,7 @@
#include <jsapi.h> // for JS_DefineFunctions, 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"
@@ -194,8 +195,33 @@ bool drain_microtask_queue(JSContext* cx, unsigned argc, JS::Value* vp) {
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;
+ }
+
+ JS::RootedFunction func(cx, JS_GetObjectFunction(callback));
+
+ GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
+ if (!priv->set_main_loop_hook(func)) {
+ 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_FS_END};
+ 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) {
diff --git a/js.gresource.xml b/js.gresource.xml
index e12dea125..87fd0fa65 100644
--- a/js.gresource.xml
+++ b/js.gresource.xml
@@ -20,6 +20,7 @@
<file>modules/esm/console.js</file>
<file>modules/esm/gi.js</file>
<file>modules/esm/system.js</file>
+ <file>modules/esm/mainloop.js</file>
<!-- Script-based Modules -->
<file>modules/script/_bootstrap/debugger.js</file>
diff --git a/modules/core/overrides/GLib.js b/modules/core/overrides/GLib.js
index cb8f177e7..7824f5772 100644
--- a/modules/core/overrides/GLib.js
+++ b/modules/core/overrides/GLib.js
@@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2011 Giovanni Campagna
const ByteArray = imports.byteArray;
+const {setMainLoopHook} = imports._promiseNative;
let GLib;
@@ -261,6 +262,19 @@ function _init() {
GLib = this;
+ GLib.MainLoop.prototype.runAsync = function (...args) {
+ return new Promise((resolve, reject) => {
+ setMainLoopHook(() => {
+ try {
+ this.run(...args);
+ resolve();
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+ };
+
// For convenience in property min or max values, since GLib.MAXINT64 and
// friends will log a warning when used
this.MAXINT64_BIGINT = 0x7fff_ffff_ffff_ffffn;
@@ -321,37 +335,37 @@ function _init() {
};
this.log_structured =
- /**
- * @param {string} logDomain
- * @param {GLib.LogLevelFlags} logLevel
- * @param {Record<string, unknown>} stringFields
- * @returns {void}
- */
- function log_structured(logDomain, logLevel, stringFields) {
- /** @type {Record<string, GLib.Variant>} */
- let fields = {};
-
- for (let key in stringFields) {
- const field = stringFields[key];
-
- if (field instanceof Uint8Array) {
- fields[key] = new GLib.Variant('ay', field);
- } else if (typeof field === 'string') {
- fields[key] = new GLib.Variant('s', field);
- } else if (field instanceof GLib.Variant) {
- // GLib.log_variant converts all Variants that are
- // not 'ay' or 's' type to strings by printing
- // them.
- //
- //
https://gitlab.gnome.org/GNOME/glib/-/blob/a380bfdf93cb3bfd3cd4caedc0127c4e5717545b/glib/gmessages.c#L1894
- fields[key] = field;
- } else {
- throw new TypeError(`Unsupported value ${field}, log_structured supports GLib.Variant,
Uint8Array, and string values.`);
+ /**
+ * @param {string} logDomain
+ * @param {GLib.LogLevelFlags} logLevel
+ * @param {Record<string, unknown>} stringFields
+ * @returns {void}
+ */
+ function log_structured(logDomain, logLevel, stringFields) {
+ /** @type {Record<string, GLib.Variant>} */
+ let fields = {};
+
+ for (let key in stringFields) {
+ const field = stringFields[key];
+
+ if (field instanceof Uint8Array) {
+ fields[key] = new GLib.Variant('ay', field);
+ } else if (typeof field === 'string') {
+ fields[key] = new GLib.Variant('s', field);
+ } else if (field instanceof GLib.Variant) {
+ // GLib.log_variant converts all Variants that are
+ // not 'ay' or 's' type to strings by printing
+ // them.
+ //
+ //
https://gitlab.gnome.org/GNOME/glib/-/blob/a380bfdf93cb3bfd3cd4caedc0127c4e5717545b/glib/gmessages.c#L1894
+ fields[key] = field;
+ } else {
+ throw new TypeError(`Unsupported value ${field}, log_structured supports GLib.Variant,
Uint8Array, and string values.`);
+ }
}
- }
- GLib.log_variant(logDomain, logLevel, new GLib.Variant('a{sv}', fields));
- };
+ GLib.log_variant(logDomain, logLevel, new GLib.Variant('a{sv}', fields));
+ };
// GjsPrivate depends on GLib so we cannot import it
// before GLib is fully resolved.
diff --git a/modules/core/overrides/Gio.js b/modules/core/overrides/Gio.js
index be2e52470..6b382bfc8 100644
--- a/modules/core/overrides/Gio.js
+++ b/modules/core/overrides/Gio.js
@@ -4,6 +4,7 @@
var GLib = imports.gi.GLib;
var GjsPrivate = imports.gi.GjsPrivate;
var Signals = imports.signals;
+var {setMainLoopHook} = imports._promiseNative;
var Gio;
// Ensures that a Gio.UnixFDList being passed into or out of a DBus method with
@@ -442,6 +443,19 @@ function _promisify(proto, asyncFunc,
function _init() {
Gio = this;
+ Gio.Application.prototype.runAsync = function (...args) {
+ return new Promise((resolve, reject) => {
+ setMainLoopHook(() => {
+ try {
+ this.run(...args);
+ resolve();
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+ };
+
Gio.DBus = {
get session() {
return Gio.bus_get_sync(Gio.BusType.SESSION, null);
diff --git a/modules/core/overrides/Gtk.js b/modules/core/overrides/Gtk.js
index ce63ba4e7..5dfc0e69a 100644
--- a/modules/core/overrides/Gtk.js
+++ b/modules/core/overrides/Gtk.js
@@ -5,6 +5,7 @@
const Legacy = imports._legacy;
const {Gio, GjsPrivate, GObject} = imports.gi;
const {_registerType} = imports._common;
+const {setMainLoopHook} = imports._promiseNative;
let Gtk;
let BuilderScope;
@@ -12,6 +13,19 @@ let BuilderScope;
function _init() {
Gtk = this;
+ Gtk.Application.prototype.runAsync = function (...args) {
+ return new Promise((resolve, reject) => {
+ setMainLoopHook(() => {
+ try {
+ this.run(...args);
+ resolve();
+ } catch (error) {
+ reject(error);
+ }
+ });
+ });
+ };
+
Gtk.children = GObject.__gtkChildren__;
Gtk.cssName = GObject.__gtkCssName__;
Gtk.internalChildren = GObject.__gtkInternalChildren__;
diff --git a/modules/esm/mainloop.js b/modules/esm/mainloop.js
new file mode 100644
index 000000000..3fd6cc14f
--- /dev/null
+++ b/modules/esm/mainloop.js
@@ -0,0 +1,44 @@
+import GLib from 'gi://GLib';
+
+const {idle_source} = imports.mainloop;
+
+export let mainloop = GLib.MainLoop.new(null, false);
+
+/**
+ * Run the mainloop asynchronously
+ *
+ * @returns {Promise<void>}
+ */
+export function run() {
+ return mainloop.runAsync();
+}
+
+/**
+ * Quit the mainloop
+ */
+export function quit() {
+ if (!mainloop.is_running())
+ throw new Error('Main loop was stopped already');
+
+ mainloop.quit();
+}
+
+/**
+ * Adds an idle task to the mainloop
+ *
+ * @param {(...args) => boolean} handler a callback to call when no higher priority events remain
+ * @param {number} [priority] the priority to queue the task with
+ */
+export function idle(handler, priority) {
+ const source = idle_source(handler, priority);
+ source.attach(null);
+
+ return source;
+}
+
+export default {
+ mainloop,
+ idle,
+ run,
+ quit,
+};
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]