LCOV - code coverage report
Current view: top level - gjs - promise.cpp (source / functions) Coverage Total Hit
Test: gjs- Code Coverage Lines: 90.0 % 80 72
Test Date: 2024-04-20 17:42:51 Functions: 100.0 % 19 19
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 62.5 % 16 10

             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                 :             : }
        

Generated by: LCOV version 2.0-1