[gjs: 6/8] object: Avoid UTF-8 conversion on every property get/set



commit 8b23901f12accfabe4582f6953865a772beb200a
Author: Philip Chimento <philip chimento gmail com>
Date:   Sat May 26 00:08:11 2018 -0700

    object: Avoid UTF-8 conversion on every property get/set
    
    Keep a cache mapping JSString to GParamSpec, and use that to avoid
    converting property names to UTF-8 and hyphenating them on every property
    access.
    
    This patch borrows heavily from some of Giovanni's work that never got
    merged:
    https://bugzilla.gnome.org/show_bug.cgi?id=690450
    
    See #160.

 gi/object.cpp                                      | 318 +++++++++++++--------
 installed-tests/js/testGObjectDestructionAccess.js |   2 +-
 2 files changed, 204 insertions(+), 116 deletions(-)
---
diff --git a/gi/object.cpp b/gi/object.cpp
index 96b5a688..cd2ed5a9 100644
--- a/gi/object.cpp
+++ b/gi/object.cpp
@@ -54,6 +54,8 @@
 #include <util/log.h>
 #include <girepository.h>
 
+#include "js/GCHashTable.h"
+
 typedef class GjsListLink GjsListLink;
 typedef struct ObjectInstance ObjectInstance;
 
@@ -113,6 +115,20 @@ class GjsListLink {
     }
 };
 
+using ParamRef = std::unique_ptr<GParamSpec, decltype(&g_param_spec_unref)>;
+using PropertyCache = JS::GCHashMap<JS::Heap<JSString *>, ParamRef,
+                                    js::DefaultHasher<JSString *>,
+                                    js::SystemAllocPolicy>;
+using FieldCache = JS::GCHashMap<JS::Heap<JSString *>, GjsAutoInfo<GIFieldInfo>,
+                                 js::DefaultHasher<JSString *>,
+                                 js::SystemAllocPolicy>;
+
+/* This tells the GCHashMap that the GC doesn't need to care about ParamRef */
+namespace JS {
+template<>
+struct GCPolicy<ParamRef> : public IgnoreGCPolicy<ParamRef> {};
+};
+
 struct ObjectInstance {
     GIObjectInfo *info;
     GObject *gobj; /* NULL if we are the prototype and not an instance */
@@ -140,7 +156,6 @@ struct ObjectInstance {
 
 static std::stack<JS::PersistentRootedObject> object_init_list;
 
-using ParamRef = std::unique_ptr<GParamSpec, decltype(&g_param_spec_unref)>;
 using ParamRefArray = std::vector<ParamRef>;
 static std::unordered_map<GType, ParamRefArray> class_init_properties;
 
@@ -183,6 +198,12 @@ gjs_object_priv_quark (void)
     return val;
 }
 
+static
+G_DEFINE_QUARK(gjs::property-cache, gjs_property_cache);
+
+static
+G_DEFINE_QUARK(gjs::field-cache, gjs_field_cache);
+
 /* Plain g_type_query fails and leaves @query uninitialized for
    dynamic types.
    See https://bugzilla.gnome.org/show_bug.cgi?id=687184 and
@@ -231,51 +252,55 @@ set_object_qdata(GObject        *gobj,
     g_object_set_qdata(gobj, gjs_object_priv_quark(), priv);
 }
 
+static PropertyCache *
+get_property_cache(ObjectInstance *proto_priv)
+{
+    void *data = g_type_get_qdata(proto_priv->gtype, gjs_property_cache_quark());
+    return static_cast<PropertyCache *>(data);
+}
+
+static FieldCache *
+get_field_cache(ObjectInstance *proto_priv)
+{
+    void *data = g_type_get_qdata(proto_priv->gtype, gjs_field_cache_quark());
+    return static_cast<FieldCache *>(data);
+}
+
 static GParamSpec *
-param_spec_from_js_prop_name(const char *js_prop_name,
-                             GType       gtype)
+find_param_spec_from_id(JSContext       *cx,
+                        ObjectInstance  *proto_priv,
+                        JS::HandleString key)
 {
     char *gname;
-    GParamSpec *param_spec;
     void *klass;
 
-    gname = gjs_hyphen_from_camel(js_prop_name);
-    gjs_debug_jsprop(GJS_DEBUG_GOBJECT,
-                     "Hyphen name %s on %s", gname, g_type_name(gtype));
+    /* First check for the ID in the cache */
+    PropertyCache *cache = get_property_cache(proto_priv);
+    g_assert(cache);
+    auto entry = cache->lookupForAdd(key);
+    if (entry)
+        return entry->value().get();
 
+    GjsAutoJSChar js_prop_name;
+    if (!gjs_string_to_utf8(cx, JS::StringValue(key), &js_prop_name))
+        return nullptr;
+
+    GType gtype = proto_priv->gtype;
+    gname = gjs_hyphen_from_camel(js_prop_name);
     klass = g_type_class_ref(gtype);
-    param_spec = g_object_class_find_property(G_OBJECT_CLASS(klass),
-                                              gname);
+    ParamRef param_spec(g_object_class_find_property(G_OBJECT_CLASS(klass), gname),
+                        g_param_spec_unref);
     g_type_class_unref(klass);
     g_free(gname);
-    return param_spec;
-}
-
-static bool
-init_g_param_from_property(JSContext      *context,
-                           GParamSpec     *param_spec,
-                           JS::HandleValue value,
-                           GParameter     *parameter)
-{
-    if ((param_spec->flags & G_PARAM_WRITABLE) == 0) {
-        /* prevent setting the prop even in JS */
-        gjs_throw(context, "GObject property %s is not writable",
-                  param_spec->name);
-        return false;
-    }
-
-    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Syncing GObject prop %s",
-                     param_spec->name);
 
-    g_value_init(&parameter->value, G_PARAM_SPEC_VALUE_TYPE(param_spec));
-    if (!gjs_value_to_g_value(context, value, &parameter->value)) {
-        g_value_unset(&parameter->value);
-        return false;
+    if (!param_spec) {
+        _gjs_proxy_throw_nonexistent_field(cx, proto_priv->gtype, js_prop_name);
+        return nullptr;
     }
 
-    parameter->name = param_spec->name;
-
-    return true;
+    if (!cache->add(entry, key, std::move(param_spec)))
+        g_error("Out of memory adding param spec to cache");
+    return entry->value().get();  /* owned by property cache */
 }
 
 static inline ObjectInstance *
@@ -325,14 +350,12 @@ object_prop_getter(JSContext *cx,
 {
     GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
 
-    /* FIXME: avoid utf8 conversion on every property access */
-    GjsAutoJSChar name;
-    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
-                            &name))
-        return false;
+    JS::RootedString name(cx,
+        gjs_dynamic_property_private_slot(&args.callee()).toString());
 
     gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Property getter '%s', obj %p priv %p",
-                     name.get(), gjs_debug_object(obj).c_str(), priv);
+                     gjs_debug_string(name).c_str(),
+                     gjs_debug_object(obj).c_str(), priv);
 
     if (!priv->gobj)  /* prototype, not an instance. */
         return true;
@@ -347,19 +370,13 @@ object_prop_getter(JSContext *cx,
         return true;
     }
 
-    char *gname;
     GParamSpec *param;
     GValue gvalue = { 0, };
 
-    gname = gjs_hyphen_from_camel(name);
-    param = g_object_class_find_property(G_OBJECT_GET_CLASS(priv->gobj),
-                                         gname);
-    g_free(gname);
+    param = find_param_spec_from_id(cx, proto_priv_from_js(cx, obj), name);
 
-    if (param == NULL) {
-        /* leave value_p as it was */
-        return true;
-    }
+    /* This is guaranteed because we resolved the property before */
+    g_assert(param);
 
     /* Do not fetch JS overridden properties from GObject, to avoid
      * infinite recursion. */
@@ -411,6 +428,35 @@ lookup_field_info(GIObjectInfo *info,
     return retval;
 }
 
+static GIFieldInfo *
+find_field_info_from_id(JSContext       *cx,
+                        ObjectInstance  *proto_priv,
+                        JS::HandleString key)
+{
+    /* First check for the ID in the cache */
+    FieldCache *cache = get_field_cache(proto_priv);
+    g_assert(cache);
+    auto entry = cache->lookupForAdd(key);
+    if (entry)
+        return entry->value().get();
+
+    GjsAutoJSChar js_prop_name;
+    if (!gjs_string_to_utf8(cx, JS::StringValue(key), &js_prop_name))
+        return nullptr;
+
+    GjsAutoInfo<GIFieldInfo> field =
+        lookup_field_info(proto_priv->info, js_prop_name);
+
+    if (!field) {
+        _gjs_proxy_throw_nonexistent_field(cx, proto_priv->gtype, js_prop_name);
+        return nullptr;
+    }
+
+    if (!cache->add(entry, key, std::move(field)))
+        g_error("Out of memory adding field info to cache");
+    return entry->value().get();  /* owned by field cache */
+}
+
 static bool
 object_field_getter(JSContext *cx,
                     unsigned   argc,
@@ -418,14 +464,12 @@ object_field_getter(JSContext *cx,
 {
     GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
 
-    /* FIXME: avoid utf8 conversion on every property access */
-    GjsAutoJSChar name;
-    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
-                            &name))
-        return false;
+    JS::RootedString name(cx,
+        gjs_dynamic_property_private_slot(&args.callee()).toString());
 
     gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Field getter '%s', obj %p priv %p",
-                     name.get(), gjs_debug_object(obj).c_str(), priv);
+                     gjs_debug_string(name).c_str(),
+                     gjs_debug_object(obj).c_str(), priv);
 
     if (priv->gobj == NULL) /* prototype, not an instance. */
         return true;
@@ -440,13 +484,10 @@ object_field_getter(JSContext *cx,
         return true;
     }
 
-    if (priv->info == NULL)
-        return true;  /* Not resolved, but no error; leave value_p untouched */
-
-    GIFieldInfo *field = lookup_field_info(priv->info, name);
-
-    if (field == NULL)
-        return true;
+    GIFieldInfo *field = find_field_info_from_id(cx, proto_priv_from_js(cx, obj),
+                                                 name);
+    /* This is guaranteed because we resolved the property before */
+    g_assert(field);
 
     bool retval = true;
     GITypeInfo *type = NULL;
@@ -454,7 +495,7 @@ object_field_getter(JSContext *cx,
     GIArgument arg = { 0 };
 
     gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Overriding %s with GObject field",
-                     name.get());
+                     gjs_debug_string(name).c_str());
 
     type = g_field_info_get_type(field);
     tag = g_type_info_get_tag(type);
@@ -465,15 +506,16 @@ object_field_getter(JSContext *cx,
         tag == GI_TYPE_TAG_GHASH ||
         tag == GI_TYPE_TAG_ERROR) {
         gjs_throw(cx, "Can't get field %s; GObject introspection supports only "
-                  "fields with simple types, not %s", name.get(),
-                  g_type_tag_to_string(tag));
+                  "fields with simple types, not %s",
+                  gjs_debug_string(name).c_str(), g_type_tag_to_string(tag));
         retval = false;
         goto out;
     }
 
     retval = g_field_info_get_field(field, priv->gobj, &arg);
     if (!retval) {
-        gjs_throw(cx, "Error getting field %s from object", name.get());
+        gjs_throw(cx, "Error getting field %s from object",
+                  gjs_debug_string(name).c_str());
         goto out;
     }
 
@@ -484,7 +526,6 @@ object_field_getter(JSContext *cx,
 out:
     if (type != NULL)
         g_base_info_unref((GIBaseInfo *) type);
-    g_base_info_unref((GIBaseInfo *) field);
     return retval;
 }
 
@@ -497,14 +538,12 @@ object_prop_setter(JSContext *cx,
 {
     GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
 
-    /* FIXME: avoid utf8 conversion on every property access */
-    GjsAutoJSChar name;
-    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
-                            &name))
-        return false;
+    JS::RootedString name(cx,
+        gjs_dynamic_property_private_slot(&args.callee()).toString());
 
     gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Property setter '%s', obj %p priv %p",
-                     name.get(), gjs_debug_object(obj).c_str(), priv);
+                     gjs_debug_string(name).c_str(),
+                     gjs_debug_object(obj).c_str(), priv);
 
     if (priv->gobj == NULL) /* prototype, not an instance. */
         return true;
@@ -521,25 +560,36 @@ object_prop_setter(JSContext *cx,
         return true;
     }
 
-    GParameter param = { NULL, { 0, }};
-
-    GType gtype = G_TYPE_FROM_INSTANCE(priv->gobj);
-    GParamSpec *param_spec = param_spec_from_js_prop_name(name, gtype);
+    GParamSpec *param_spec =
+        find_param_spec_from_id(cx, proto_priv_from_js(cx, obj), name);
     if (!param_spec)
-        return true;
+        return false;
 
     /* Do not set JS overridden properties through GObject, to avoid
      * infinite recursion (unless constructing) */
     if (g_param_spec_get_qdata(param_spec, gjs_is_custom_property_quark()))
         return true;
 
-    if (!init_g_param_from_property(cx, param_spec, args[0], &param))
+    if (!(param_spec->flags & G_PARAM_WRITABLE))
+        /* prevent setting the prop even in JS */
+        return _gjs_proxy_throw_readonly_field(cx, priv->gtype, param_spec->name);
+
+    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Setting GObject prop %s",
+                     param_spec->name);
+
+    GValue value = G_VALUE_INIT;
+    g_value_init(&value, G_PARAM_SPEC_VALUE_TYPE(param_spec));
+    if (!gjs_value_to_g_value(cx, args[0], &value)) {
+        g_value_unset(&value);
         return false;
+    }
+
+    g_object_set_property(priv->gobj, param_spec->name, &value);
+    g_value_unset(&value);
 
-    g_object_set_property(priv->gobj, param.name,
-                          &param.value);
+    /* Clear the JS stored value, to avoid keeping additional references */
+    args.rval().setUndefined();
 
-    g_value_unset(&param.value);
     return true;
 }
 
@@ -550,11 +600,12 @@ object_field_setter(JSContext *cx,
 {
     GJS_GET_PRIV(cx, argc, vp, args, obj, ObjectInstance, priv);
 
-    /* FIXME: avoid utf8 conversion on every property access */
-    GjsAutoJSChar name;
-    if (!gjs_string_to_utf8(cx, gjs_dynamic_property_private_slot(&args.callee()),
-                            &name))
-        return false;
+    JS::RootedString name(cx,
+        gjs_dynamic_property_private_slot(&args.callee()).toString());
+
+    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Field setter '%s', obj %p priv %p",
+                     gjs_debug_string(name).c_str(),
+                     gjs_debug_object(obj).c_str(), priv);
 
     if (priv->gobj == NULL) /* prototype, not an instance. */
         return true;
@@ -569,38 +620,27 @@ object_field_setter(JSContext *cx,
         return true;
     }
 
-    gjs_debug_jsprop(GJS_DEBUG_GOBJECT, "Field setter '%s', obj %p priv %p",
-                     name.get(), gjs_debug_object(obj).c_str(), priv);
-
-    if (priv->info == NULL)
-        return true;
-
-    GIFieldInfo *field = lookup_field_info(priv->info, name);
+    GIFieldInfo *field =
+        find_field_info_from_id(cx, proto_priv_from_js(cx, obj), name);
     if (field == NULL)
-        return true;
-
-    bool retval = true;
+        return false;
 
     /* As far as I know, GI never exposes GObject instance struct fields as
      * writable, so no need to implement this for the time being */
     if (g_field_info_get_flags(field) & GI_FIELD_IS_WRITABLE) {
         g_message("Field %s of a GObject is writable, but setting it is not "
-                  "implemented", name.get());
-        goto out;
+                  "implemented", gjs_debug_string(name).c_str());
+        return true;
     }
 
-    retval = _gjs_proxy_throw_readonly_field(cx, priv->gtype,
-                                             g_base_info_get_name(field));
-
     /* We have to update args.rval(), because JS caches it as the property's "stored
      * value" 
(https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference/Stored_value)
      * and so subsequent gets would get the stored value instead of accessing
      * the field */
     args.rval().setUndefined();
 
-out:
-    g_base_info_unref((GIBaseInfo *) field);
-    return retval;
+    return _gjs_proxy_throw_readonly_field(cx, priv->gtype,
+                                           g_base_info_get_name(field));
 }
 
 static bool
@@ -1010,7 +1050,7 @@ static bool
 object_instance_props_to_g_parameters(JSContext                  *context,
                                       JSObject                   *obj,
                                       const JS::HandleValueArray& args,
-                                      GType                       gtype,
+                                      ObjectInstance             *proto_priv,
                                       std::vector<GParameter>&    gparams)
 {
     size_t ix, length;
@@ -1040,20 +1080,35 @@ object_instance_props_to_g_parameters(JSContext                  *context,
          * doesn't know that */
         prop_id = ids[ix];
 
-        if (!gjs_object_require_property(context, props, "property list",
-                                         prop_id, &value) ||
-            !gjs_get_string_id(context, prop_id, &name))
+        if (!JSID_IS_STRING(prop_id))
+            return _gjs_proxy_throw_nonexistent_field(context, proto_priv->gtype,
+                                                      gjs_debug_id(prop_id).c_str());
+
+        JS::RootedString js_prop_name(context, JSID_TO_STRING(prop_id));
+        GParamSpec *param_spec = find_param_spec_from_id(context, proto_priv,
+                                                         js_prop_name);
+        if (!param_spec)
             return false;
 
-        GParamSpec *param_spec = param_spec_from_js_prop_name(name, gtype);
-        if (!param_spec) {
-            gjs_throw(context, "No property %s on this GObject %s",
-                      name.get(), g_type_name(gtype));
+        if (!JS_GetPropertyById(context, props, prop_id, &value))
+            return false;
+        if (value.isUndefined()) {
+            gjs_throw(context, "Invalid value 'undefined' for property %s in "
+                      "object initializer.", param_spec->name);
             return false;
         }
 
-        if (!init_g_param_from_property(context, param_spec, value, &gparam))
+        if (!(param_spec->flags & G_PARAM_WRITABLE))
+            return _gjs_proxy_throw_readonly_field(context, proto_priv->gtype,
+                                                   param_spec->name);
+            /* prevent setting the prop even in JS */
+
+        gparam.name = param_spec->name;
+        g_value_init(&gparam.value, G_PARAM_SPEC_VALUE_TYPE(param_spec));
+        if (!gjs_value_to_g_value(context, value, &gparam.value)) {
+            g_value_unset(&gparam.value);
             return false;
+        }
 
         gparams.push_back(gparam);
     }
@@ -1542,7 +1597,8 @@ object_instance_init (JSContext                  *context,
     g_assert(gtype != G_TYPE_NONE);
 
     if (!object_instance_props_to_g_parameters(context, object, args,
-                                               gtype, params)) {
+                                               proto_priv_from_js(context, object),
+                                               params)) {
         clear_g_params(params);
         return false;
     }
@@ -1659,6 +1715,13 @@ object_instance_trace(JSTracer *tracer,
 
     for (GClosure *closure : priv->closures)
         gjs_closure_trace(closure, tracer);
+
+    PropertyCache *property_cache = get_property_cache(priv);
+    if (property_cache)
+        property_cache->trace(tracer);
+    FieldCache *field_cache = get_field_cache(priv);
+    if (field_cache)
+        field_cache->trace(tracer);
 }
 
 static void
@@ -1685,6 +1748,20 @@ object_instance_finalize(JSFreeOp  *fop,
                                     priv->info ? g_base_info_get_namespace((GIBaseInfo*) priv->info) : 
"_gjs_private",
                                     priv->info ? g_base_info_get_name((GIBaseInfo*) priv->info) : 
g_type_name(priv->gtype)));
 
+    /* Prototype only, although if priv->gobj is null it could also be an
+     * instance struct with a freed gobject. This is annoying. It means we have
+     * to always do this, and have an extra null check. */
+    PropertyCache *property_cache = get_property_cache(priv);
+    if (property_cache) {
+        delete property_cache;
+        g_type_set_qdata(priv->gtype, gjs_property_cache_quark(), nullptr);
+    }
+    FieldCache *field_cache = get_field_cache(priv);
+    if (field_cache) {
+        delete field_cache;
+        g_type_set_qdata(priv->gtype, gjs_field_cache_quark(), nullptr);
+    }
+
     /* This applies only to instances, not prototypes, but it's possible that
      * an instance's GObject is already freed at this point. */
     invalidate_all_closures(priv);
@@ -2264,6 +2341,17 @@ gjs_define_object_class(JSContext              *context,
         g_base_info_ref((GIBaseInfo*) info);
     priv->gtype = gtype;
     priv->klass = (GTypeClass*) g_type_class_ref (gtype);
+
+    auto *property_cache = new PropertyCache();
+    if (!property_cache->init())
+        g_error("Out of memory for property cache of %s", g_type_name(gtype));
+    g_type_set_qdata(gtype, gjs_property_cache_quark(), property_cache);
+
+    auto *field_cache = new FieldCache();
+    if (!field_cache->init())
+        g_error("Out of memory for field cache of %s", g_type_name(gtype));
+    g_type_set_qdata(gtype, gjs_field_cache_quark(), field_cache);
+
     JS_SetPrivate(prototype, priv);
 
     gjs_debug(GJS_DEBUG_GOBJECT, "Defined class for %s (%s), prototype %p, "
diff --git a/installed-tests/js/testGObjectDestructionAccess.js 
b/installed-tests/js/testGObjectDestructionAccess.js
index e479ff9a..af6bb592 100644
--- a/installed-tests/js/testGObjectDestructionAccess.js
+++ b/installed-tests/js/testGObjectDestructionAccess.js
@@ -18,7 +18,7 @@ describe('Access to destroyed GObject', () => {
 
     it('Get property', () => {
         GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_CRITICAL,
-            'Object Gtk.Window (0x*');
+            'Object GtkWindow (0x*');
 
         let title = destroyedWindow.title;
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]