Branch data Line data Source code
1 : : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
2 : : // SPDX-FileCopyrightText: 2021 Evan Welsh <contact@evanwelsh.com>
3 : : // SPDX-FileCopyrightText: 2021 Marco Trevisan <mail@3v1n0.net>
4 : :
5 : : #include <config.h>
6 : :
7 : : #include <stddef.h> // for size_t
8 : :
9 : : #include <string> // for string methods
10 : :
11 : : #include <gio/gio.h>
12 : : #include <glib-object.h>
13 : :
14 : : #include <js/CallAndConstruct.h> // for JS::IsCallable
15 : : #include <js/CallArgs.h>
16 : : #include <js/PropertyAndElement.h> // for JS_DefineFunctions
17 : : #include <js/PropertySpec.h>
18 : : #include <js/RootingAPI.h>
19 : : #include <js/TypeDecls.h>
20 : : #include <jsapi.h> // for JS_NewPlainObject
21 : : #include <jsfriendapi.h> // for RunJobs
22 : :
23 : : #include "gjs/auto.h"
24 : : #include "gjs/context-private.h"
25 : : #include "gjs/jsapi-util-args.h"
26 : : #include "gjs/jsapi-util.h"
27 : : #include "gjs/macros.h"
28 : : #include "gjs/promise.h"
29 : : #include "util/log.h"
30 : :
31 : : /**
32 : : * promise.cpp - This file implements a custom GSource, PromiseJobQueueSource,
33 : : * which handles promise dispatching within GJS. Custom GSources are able to
34 : : * control under which conditions they dispatch. PromiseJobQueueSource will
35 : : * always dispatch if even a single Promise is enqueued and will continue
36 : : * dispatching until all Promises (also known as "Jobs" within SpiderMonkey)
37 : : * are run. While this does technically mean Promises can starve the mainloop
38 : : * if run recursively, this is intentional. Within JavaScript Promises are
39 : : * considered "microtasks" and a microtask must run before any other task
40 : : * continues.
41 : : *
42 : : * PromiseJobQueueSource is attached to the thread's default GMainContext with
43 : : * a default priority of -1000. This is 10x the priority of G_PRIORITY_HIGH and
44 : : * no application code should attempt to override this.
45 : : *
46 : : * See doc/Custom-GSources.md for more background information on custom
47 : : * GSources and microtasks
48 : : */
49 : :
50 : : namespace Gjs {
51 : :
52 : : /**
53 : : * PromiseJobDispatcher::Source:
54 : : *
55 : : * A custom GSource which handles draining our job queue.
56 : : */
57 : : class PromiseJobDispatcher::Source : public GSource {
58 : : // The private GJS context this source runs within.
59 : : GjsContextPrivate* m_gjs;
60 : : // The main context this source attaches to.
61 : : AutoMainContext m_main_context;
62 : : // The cancellable that stops this source.
63 : : AutoUnref<GCancellable> m_cancellable;
64 : 3680 : AutoPointer<GSource, GSource, g_source_unref> m_cancellable_source;
65 : :
66 : : // G_PRIORITY_HIGH is normally -100, we set 10 times that to ensure our
67 : : // source always has the greatest priority. This means our prepare will
68 : : // be called before other sources, and prepare will determine whether
69 : : // we dispatch.
70 : : static constexpr int PRIORITY = 10 * G_PRIORITY_HIGH;
71 : :
72 : : // GSource custom functions
73 : : static GSourceFuncs source_funcs;
74 : :
75 : : // Called to determine whether the source should run (dispatch) in the
76 : : // next event loop iteration. If the job queue is not empty we return true
77 : : // to schedule a dispatch.
78 : 11394 : gboolean prepare(int* timeout [[maybe_unused]]) { return !m_gjs->empty(); }
79 : :
80 : 524 : gboolean dispatch() {
81 [ - + ]: 524 : if (g_cancellable_is_cancelled(m_cancellable))
82 : 0 : return G_SOURCE_REMOVE;
83 : :
84 : : // The ready time is sometimes set to 0 to kick us out of polling,
85 : : // we need to reset the value here or this source will always be the
86 : : // next one to execute. (it will starve the other sources)
87 : 524 : g_source_set_ready_time(this, -1);
88 : :
89 : : // Drain the job queue.
90 : 524 : js::RunJobs(m_gjs->context());
91 : :
92 : 524 : return G_SOURCE_CONTINUE;
93 : : }
94 : :
95 : : public:
96 : : /**
97 : : * Source::Source:
98 : : * @gjs: the GJS object
99 : : * @main_context: GLib main context to associate with the source
100 : : *
101 : : * Constructs a new GSource for the PromiseJobDispatcher and adds a
102 : : * reference to the associated main context.
103 : : */
104 : 255 : Source(GjsContextPrivate* gjs, GMainContext* main_context)
105 : 255 : : m_gjs(gjs),
106 : 255 : m_main_context(main_context, TakeOwnership{}),
107 : 255 : m_cancellable(g_cancellable_new()),
108 : 255 : m_cancellable_source(g_cancellable_source_new(m_cancellable)) {
109 : 255 : g_source_set_priority(this, PRIORITY);
110 : 255 : g_source_set_static_name(this, "GjsPromiseJobQueueSource");
111 : :
112 : : // Add our cancellable source to our main source,
113 : : // this will trigger the main source if our cancellable
114 : : // is cancelled.
115 : 255 : g_source_add_child_source(this, m_cancellable_source);
116 : 255 : }
117 : :
118 : 255 : void* operator new(size_t size) {
119 : 255 : return g_source_new(&source_funcs, size);
120 : : }
121 : 253 : void operator delete(void* p) { g_source_unref(static_cast<GSource*>(p)); }
122 : :
123 : 9935 : bool is_running() { return !!g_source_get_context(this); }
124 : :
125 : : /**
126 : : * Source::cancel:
127 : : *
128 : : * Trigger the cancellable, detaching our source.
129 : : */
130 : 3695 : void cancel() { g_cancellable_cancel(m_cancellable); }
131 : : /**
132 : : * Source::uncancel:
133 : : *
134 : : * Reset the cancellable and prevent the source from stopping, overriding a
135 : : * previous cancel() call. Called by PromiseJobDispatcher::start() to ensure
136 : : * the custom source will start.
137 : : */
138 : 5105 : void uncancel() {
139 [ + + ]: 5105 : if (!g_cancellable_is_cancelled(m_cancellable))
140 : 1678 : return;
141 : :
142 : 3427 : gjs_debug(GJS_DEBUG_MAINLOOP, "Uncancelling promise job dispatcher");
143 : :
144 [ + - ]: 3427 : if (is_running())
145 : 3427 : g_source_remove_child_source(this, m_cancellable_source);
146 : : else
147 : 0 : g_source_destroy(m_cancellable_source);
148 : :
149 : : // Drop the old cancellable and create a new one, as per
150 : : // https://docs.gtk.org/gio/method.Cancellable.reset.html
151 : 3427 : m_cancellable = g_cancellable_new();
152 : 3427 : m_cancellable_source = g_cancellable_source_new(m_cancellable);
153 : 3427 : g_source_add_child_source(this, m_cancellable_source);
154 : : }
155 : : };
156 : :
157 : : GSourceFuncs PromiseJobDispatcher::Source::source_funcs = {
158 : 11394 : [](GSource* source, int* timeout) {
159 : 11394 : return static_cast<Source*>(source)->prepare(timeout);
160 : : },
161 : : nullptr, // check
162 : 524 : [](GSource* source, GSourceFunc, void*) {
163 : 524 : return static_cast<Source*>(source)->dispatch();
164 : : },
165 : 253 : [](GSource* source) { static_cast<Source*>(source)->~Source(); },
166 : : };
167 : :
168 : 255 : PromiseJobDispatcher::PromiseJobDispatcher(GjsContextPrivate* gjs)
169 : : // Acquire a guaranteed reference to this thread's default main context
170 : 255 : : m_main_context(g_main_context_ref_thread_default()),
171 : : // Create and reference our custom GSource
172 : 255 : m_source(std::make_unique<Source>(gjs, m_main_context)) {}
173 : :
174 : 253 : PromiseJobDispatcher::~PromiseJobDispatcher() {
175 : 253 : g_source_destroy(m_source.get());
176 : 253 : }
177 : :
178 : 6508 : bool PromiseJobDispatcher::is_running() { return m_source->is_running(); }
179 : :
180 : 5105 : void PromiseJobDispatcher::start() {
181 : : // Reset the cancellable
182 : 5105 : m_source->uncancel();
183 : :
184 : : // Don't re-attach if the task is already running
185 [ + + ]: 5105 : if (is_running())
186 : 4850 : return;
187 : :
188 : 255 : gjs_debug(GJS_DEBUG_MAINLOOP, "Starting promise job dispatcher");
189 : 255 : g_source_attach(m_source.get(), m_main_context);
190 : : }
191 : :
192 : 3695 : void PromiseJobDispatcher::stop() {
193 : 3695 : gjs_debug(GJS_DEBUG_MAINLOOP, "Stopping promise job dispatcher");
194 : 3695 : m_source->cancel();
195 : 3695 : }
196 : :
197 : : }; // namespace Gjs
198 : :
199 : : GJS_JSAPI_RETURN_CONVENTION
200 : 9827 : bool drain_microtask_queue(JSContext* cx, unsigned argc, JS::Value* vp) {
201 : 9827 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
202 : :
203 : 9827 : js::RunJobs(cx);
204 : :
205 : 9827 : args.rval().setUndefined();
206 : 9827 : return true;
207 : : }
208 : :
209 : : GJS_JSAPI_RETURN_CONVENTION
210 : 51 : bool set_main_loop_hook(JSContext* cx, unsigned argc, JS::Value* vp) {
211 : 51 : JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
212 : :
213 : 51 : JS::RootedObject callback(cx);
214 [ - + ]: 51 : if (!gjs_parse_call_args(cx, "setMainLoopHook", args, "o", "callback",
215 : : &callback)) {
216 : 0 : return false;
217 : : }
218 : :
219 [ - + ]: 51 : if (!JS::IsCallable(callback)) {
220 : 0 : gjs_throw(cx, "Main loop hook must be callable");
221 : 0 : return false;
222 : : }
223 : :
224 : 51 : gjs_debug(GJS_DEBUG_MAINLOOP, "Set main loop hook to %s",
225 : 102 : gjs_debug_object(callback).c_str());
226 : :
227 : 51 : GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
228 [ - + ]: 51 : if (!priv->set_main_loop_hook(callback)) {
229 : 0 : gjs_throw(
230 : : cx,
231 : : "A mainloop is already running. Did you already call runAsync()?");
232 : 0 : return false;
233 : : }
234 : :
235 : 51 : args.rval().setUndefined();
236 : 51 : return true;
237 : 51 : }
238 : :
239 : : JSFunctionSpec gjs_native_promise_module_funcs[] = {
240 : : JS_FN("drainMicrotaskQueue", &drain_microtask_queue, 0, 0),
241 : : JS_FN("setMainLoopHook", &set_main_loop_hook, 1, 0), JS_FS_END};
242 : :
243 : 64 : bool gjs_define_native_promise_stuff(JSContext* cx,
244 : : JS::MutableHandleObject module) {
245 : 64 : module.set(JS_NewPlainObject(cx));
246 [ - + ]: 64 : if (!module)
247 : 0 : return false;
248 : 64 : return JS_DefineFunctions(cx, module, gjs_native_promise_module_funcs);
249 : : }
|