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