LCOV - code coverage report
Current view: top level - gjs - promise.cpp (source / functions) Coverage Total Hit
Test: gjs- Code Coverage Lines: 90.1 % 81 73
Test Date: 2025-02-15 06:20:10 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/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                 :             : }
        

Generated by: LCOV version 2.0-1