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

           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                 :       9121 :     gboolean prepare(int* timeout [[maybe_unused]]) { return !m_gjs->empty(); }
      74                 :            : 
      75                 :        454 :     gboolean dispatch() {
      76         [ -  + ]:        454 :         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                 :        454 :         g_source_set_ready_time(this, -1);
      83                 :            : 
      84                 :            :         // Drain the job queue.
      85                 :        454 :         js::RunJobs(m_gjs->context());
      86                 :            : 
      87                 :        454 :         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                 :        242 :     Source(GjsContextPrivate* gjs, GMainContext* main_context)
      99                 :        242 :         : m_gjs(gjs),
     100                 :        242 :           m_main_context(main_context, GjsAutoTakeOwnership()),
     101                 :        242 :           m_cancellable(g_cancellable_new()),
     102                 :        242 :           m_cancellable_source(g_cancellable_source_new(m_cancellable)) {
     103                 :        242 :         g_source_set_priority(this, PRIORITY);
     104                 :            : #if GLIB_CHECK_VERSION(2, 70, 0)
     105                 :        242 :         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                 :        242 :         g_source_add_child_source(this, m_cancellable_source);
     114                 :        242 :     }
     115                 :            : 
     116                 :        242 :     void* operator new(size_t size) {
     117                 :        242 :         return g_source_new(&source_funcs, size);
     118                 :            :     }
     119                 :        240 :     void operator delete(void* p) { g_source_unref(static_cast<GSource*>(p)); }
     120                 :            : 
     121                 :       7618 :     bool is_running() { return !!g_source_get_context(this); }
     122                 :            : 
     123                 :            :     /**
     124                 :            :      * @brief Trigger the cancellable, detaching our source.
     125                 :            :      */
     126                 :       2955 :     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                 :       3936 :     void reset() {
     133         [ +  + ]:       3936 :         if (!g_cancellable_is_cancelled(m_cancellable))
     134                 :       1230 :             return;
     135                 :            : 
     136                 :       2706 :         gjs_debug(GJS_DEBUG_MAINLOOP, "Uncancelling promise job dispatcher");
     137                 :            : 
     138         [ +  - ]:       2706 :         if (is_running())
     139                 :       2706 :             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                 :       2706 :         m_cancellable = g_cancellable_new();
     146                 :       2706 :         m_cancellable_source = g_cancellable_source_new(m_cancellable);
     147                 :       2706 :         g_source_add_child_source(this, m_cancellable_source);
     148                 :            :     }
     149                 :            : };
     150                 :            : 
     151                 :            : GSourceFuncs PromiseJobDispatcher::Source::source_funcs = {
     152                 :       9121 :     [](GSource* source, int* timeout) {
     153                 :       9121 :         return static_cast<Source*>(source)->prepare(timeout);
     154                 :            :     },
     155                 :            :     nullptr,  // check
     156                 :        454 :     [](GSource* source, GSourceFunc, void*) {
     157                 :        454 :         return static_cast<Source*>(source)->dispatch();
     158                 :            :     },
     159                 :        240 :     [](GSource* source) { static_cast<Source*>(source)->~Source(); },
     160                 :            : };
     161                 :            : 
     162                 :        242 : PromiseJobDispatcher::PromiseJobDispatcher(GjsContextPrivate* gjs)
     163                 :            :     // Acquire a guaranteed reference to this thread's default main context
     164                 :        242 :     : m_main_context(g_main_context_ref_thread_default()),
     165                 :            :       // Create and reference our custom GSource
     166                 :        242 :       m_source(std::make_unique<Source>(gjs, m_main_context)) {}
     167                 :            : 
     168                 :        240 : PromiseJobDispatcher::~PromiseJobDispatcher() {
     169                 :        240 :     g_source_destroy(m_source.get());
     170                 :        240 : }
     171                 :            : 
     172                 :       4912 : bool PromiseJobDispatcher::is_running() { return m_source->is_running(); }
     173                 :            : 
     174                 :       3936 : void PromiseJobDispatcher::start() {
     175                 :            :     // Reset the cancellable
     176                 :       3936 :     m_source->reset();
     177                 :            : 
     178                 :            :     // Don't re-attach if the task is already running
     179         [ +  + ]:       3936 :     if (is_running())
     180                 :       3694 :         return;
     181                 :            : 
     182                 :        242 :     gjs_debug(GJS_DEBUG_MAINLOOP, "Starting promise job dispatcher");
     183                 :        242 :     g_source_attach(m_source.get(), m_main_context);
     184                 :            : }
     185                 :            : 
     186                 :       2955 : void PromiseJobDispatcher::stop() {
     187                 :       2955 :     gjs_debug(GJS_DEBUG_MAINLOOP, "Stopping promise job dispatcher");
     188                 :       2955 :     m_source->cancel();
     189                 :       2955 : }
     190                 :            : 
     191                 :            : };  // namespace Gjs
     192                 :            : 
     193                 :            : GJS_JSAPI_RETURN_CONVENTION
     194                 :       7865 : bool drain_microtask_queue(JSContext* cx, unsigned argc, JS::Value* vp) {
     195                 :       7865 :     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
     196                 :            : 
     197                 :       7865 :     js::RunJobs(cx);
     198                 :            : 
     199                 :       7865 :     args.rval().setUndefined();
     200                 :       7865 :     return true;
     201                 :            : }
     202                 :            : 
     203                 :            : GJS_JSAPI_RETURN_CONVENTION
     204                 :         50 : bool set_main_loop_hook(JSContext* cx, unsigned argc, JS::Value* vp) {
     205                 :         50 :     JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
     206                 :            : 
     207                 :         50 :     JS::RootedObject callback(cx);
     208         [ -  + ]:         50 :     if (!gjs_parse_call_args(cx, "setMainLoopHook", args, "o", "callback",
     209                 :            :                              &callback)) {
     210                 :          0 :         return false;
     211                 :            :     }
     212                 :            : 
     213         [ -  + ]:         50 :     if (!JS::IsCallable(callback)) {
     214                 :          0 :         gjs_throw(cx, "Main loop hook must be callable");
     215                 :          0 :         return false;
     216                 :            :     }
     217                 :            : 
     218                 :         50 :     gjs_debug(GJS_DEBUG_MAINLOOP, "Set main loop hook to %s",
     219                 :        100 :               gjs_debug_object(callback).c_str());
     220                 :            : 
     221                 :         50 :     GjsContextPrivate* priv = GjsContextPrivate::from_cx(cx);
     222         [ -  + ]:         50 :     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                 :         50 :     args.rval().setUndefined();
     230                 :         50 :     return true;
     231                 :         50 : }
     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                 :         63 : bool gjs_define_native_promise_stuff(JSContext* cx,
     238                 :            :                                      JS::MutableHandleObject module) {
     239                 :         63 :     module.set(JS_NewPlainObject(cx));
     240         [ -  + ]:         63 :     if (!module)
     241                 :          0 :         return false;
     242                 :         63 :     return JS_DefineFunctions(cx, module, gjs_native_promise_module_funcs);
     243                 :            : }

Generated by: LCOV version 1.14