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