Просмотр исходного кода

Improve interop performance with delegates and dictionaries (#2088)

Marko Lahma 3 месяцев назад
Родитель
Сommit
e4800fad93

+ 432 - 0
Jint.Benchmark/InteropLambdaBenchmark.cs

@@ -0,0 +1,432 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Order;
+using Jint.Native;
+using Jint.Native.Function;
+
+namespace Jint.Benchmark;
+
+[RankColumn]
+[MemoryDiagnoser]
+[Orderer(SummaryOrderPolicy.FastestToSlowest)]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
+public class InteropLambdaBenchmark
+{
+    private TestData[] _testArray;
+    private TestDataRoot _root;
+    private object _data;
+    private const string FindValue = "SomeKind22222";
+
+    private const int Iterations = 10;
+
+    private Engine _engine;
+
+    private const string ScriptInline = """
+                                        function findIt(data, value) {
+                                            return data.array.find(x => x.value == value);
+                                        }
+                                        """;
+
+    private const string ScriptForLoop = """
+                                         function findIt(data, value) {
+                                             const array = data.array;
+                                             const length = array.length;
+                                             for (let i = 0; i < length; i++) {
+                                                 const item = array[i];
+                                                 if (item.value == value) {
+                                                     return item;
+                                                 }
+                                             }
+                                             
+                                             return null;
+                                         }
+                                         """;
+
+    private Function _forLoopFunction;
+    private Function _inlineFunction;
+    private Func<JsValue, JsValue[], JsValue> _inlineCSharpFunction;
+
+    [Params(TestDataType.ClrObject, TestDataType.Dictionary, TestDataType.JsonNode, TestDataType.JsValue)]
+    public TestDataType Type { get; set; }
+
+    [GlobalSetup]
+    public void GlobalSetup()
+    {
+        _engine = new Engine();
+
+        _testArray = [new TestData("SomeKind00000"), new TestData("SomeKind1111"), new TestData(FindValue)];
+        _root = new TestDataRoot(_testArray);
+
+        if (Type == TestDataType.ClrObject)
+        {
+            _data = _root;
+        }
+        else if (Type == TestDataType.JsonNode)
+        {
+            _data = JsonSerializer.SerializeToNode(_root, JsonDefaults.JsonSerializerOptions);
+        }
+        else if (Type == TestDataType.Dictionary)
+        {
+            _data = JsonSerializer.Deserialize<Dictionary<string, object>>(JsonSerializer.Serialize(_root, JsonDefaults.JsonSerializerOptions), JsonDefaults.JsonSerializerOptions);
+        }
+        else if (Type == TestDataType.JsValue)
+        {
+            _data = JsonSerializer.Deserialize<JsObject>(JsonSerializer.Serialize(_root, JsonDefaults.JsonSerializerOptions), JsonDefaults.JsonSerializerOptions);
+        }
+
+        _inlineFunction = (Function) _engine.Evaluate(ScriptInline + "findIt;");
+        _inlineCSharpFunction = (Func<JsValue, JsValue[], JsValue>) _inlineFunction.ToObject();
+        _forLoopFunction = (Function) _engine.Evaluate(ScriptForLoop + "findIt;");
+    }
+
+    [Benchmark]
+    public void InlineEngineInvoke()
+    {
+        for (var i = 0; i < Iterations; i++)
+        {
+            var value = _engine.Invoke(_inlineFunction!, [_data, FindValue]).ToObject();
+        }
+    }
+
+    [Benchmark]
+    public void Inline()
+    {
+        for (var i = 0; i < Iterations; i++)
+        {
+            var value = _inlineFunction!.Call(JsValue.FromObject(_engine, _data), JsValue.FromObject(_engine, FindValue)).ToObject();
+        }
+    }
+
+    [Benchmark]
+    public void InlineCSharp()
+    {
+        for (var i = 0; i < Iterations; i++)
+        {
+            var value = _inlineCSharpFunction(JsValue.Undefined, [JsValue.FromObject(_engine, _data), JsValue.FromObject(_engine, FindValue)]).ToObject();
+        }
+    }
+
+    [Benchmark(Baseline = true)]
+    public void ForLoop()
+    {
+        for (var i = 0; i < Iterations; i++)
+        {
+            var value = _forLoopFunction!.Call(JsValue.FromObject(_engine, _data), JsValue.FromObject(_engine, FindValue)).ToObject();
+        }
+    }
+
+    [Benchmark]
+    public void ForLoopEngineInvoke()
+    {
+        for (var i = 0; i < Iterations; i++)
+        {
+            var value = _engine.Invoke(_forLoopFunction!, [_data, FindValue]).ToObject();
+        }
+    }
+}
+
+public class TestDataRoot
+{
+    public TestData[] array { get; set; }
+
+    public TestDataRoot(TestData[] array)
+    {
+        this.array = array;
+    }
+}
+
+
+public record TestData(string value);
+
+public sealed class DictionaryStringObjectJsonConverter : JsonConverter<Dictionary<string, object>>
+{
+    public override Dictionary<string, object> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartObject)
+        {
+            throw new JsonException();
+        }
+
+        var dictionary = new Dictionary<string, object>(JsonDefaults.DictionaryCapacity);
+
+        while (reader.Read())
+        {
+            if (reader.TokenType == JsonTokenType.EndObject)
+            {
+                return dictionary;
+            }
+
+            if (reader.TokenType != JsonTokenType.PropertyName)
+            {
+                throw new JsonException();
+            }
+
+            string propertyName = reader.GetString();
+
+            reader.Read();
+
+            dictionary[propertyName] = ReadValue(ref reader, options);
+        }
+
+        throw new JsonException();
+    }
+
+    private object ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options)
+    {
+        switch (reader.TokenType)
+        {
+            case JsonTokenType.String:
+                return reader.GetString();
+            case JsonTokenType.Number:
+                if (reader.TryGetInt64(out long l))
+                {
+                    return l;
+                }
+
+                return reader.GetDouble();
+            case JsonTokenType.True:
+                return true;
+            case JsonTokenType.False:
+                return false;
+            case JsonTokenType.Null:
+                return null;
+            case JsonTokenType.StartObject:
+                return Read(ref reader, typeof(Dictionary<string, object>), options);
+            case JsonTokenType.StartArray:
+                var list = new List<object>();
+                while (reader.Read())
+                {
+                    if (reader.TokenType == JsonTokenType.EndArray)
+                    {
+                        return list;
+                    }
+
+                    list.Add(ReadValue(ref reader, options));
+                }
+
+                throw new JsonException();
+            default:
+                throw new JsonException();
+        }
+    }
+
+    public override void Write(Utf8JsonWriter writer, Dictionary<string, object> value, JsonSerializerOptions options)
+    {
+        writer.WriteStartObject();
+
+        foreach (var kvp in value)
+        {
+            writer.WritePropertyName(kvp.Key);
+            WriteValue(writer, kvp.Value, options);
+        }
+
+        writer.WriteEndObject();
+    }
+
+    private void WriteValue(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
+    {
+        switch (value)
+        {
+            case string s:
+                writer.WriteStringValue(s);
+                break;
+            case long l:
+                writer.WriteNumberValue(l);
+                break;
+            case double d:
+                writer.WriteNumberValue(d);
+                break;
+            case bool b:
+                writer.WriteBooleanValue(b);
+                break;
+            case null:
+                writer.WriteNullValue();
+                break;
+            case Dictionary<string, object> dict:
+                writer.WriteStartObject();
+                foreach (var kvp in dict)
+                {
+                    writer.WritePropertyName(kvp.Key);
+                    WriteValue(writer, kvp.Value, options);
+                }
+
+                writer.WriteEndObject();
+                break;
+            case List<object> list:
+                writer.WriteStartArray();
+                foreach (var item in list)
+                {
+                    WriteValue(writer, item, options);
+                }
+
+                writer.WriteEndArray();
+                break;
+            case JsonNode node:
+                JsonSerializer.Serialize(writer, node, options);
+                break;
+            default:
+                throw new InvalidOperationException($"Unsupported type: {value?.GetType()}");
+        }
+    }
+}
+
+public sealed class NativeJsValueJsonConverter : JsonConverter<JsObject>
+{
+    private readonly Engine _engine = new();
+
+    public override JsObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+    {
+        if (reader.TokenType != JsonTokenType.StartObject)
+        {
+            throw new JsonException();
+        }
+
+        var dictionary = new JsObject(_engine);
+
+        while (reader.Read())
+        {
+            if (reader.TokenType == JsonTokenType.EndObject)
+            {
+                return dictionary;
+            }
+
+            if (reader.TokenType != JsonTokenType.PropertyName)
+            {
+                throw new JsonException();
+            }
+
+            string propertyName = reader.GetString();
+
+            reader.Read();
+
+            dictionary[propertyName] = ReadValue(ref reader, options);
+        }
+
+        throw new JsonException();
+    }
+
+    private JsValue ReadValue(ref Utf8JsonReader reader, JsonSerializerOptions options)
+    {
+        switch (reader.TokenType)
+        {
+            case JsonTokenType.String:
+                return reader.GetString();
+            case JsonTokenType.Number:
+                if (reader.TryGetInt64(out long l))
+                {
+                    return l;
+                }
+
+                return reader.GetDouble();
+            case JsonTokenType.True:
+                return true;
+            case JsonTokenType.False:
+                return false;
+            case JsonTokenType.Null:
+                return null;
+            case JsonTokenType.StartObject:
+                return Read(ref reader, typeof(JsObject), options);
+            case JsonTokenType.StartArray:
+                var list = new JsArray(_engine);
+                while (reader.Read())
+                {
+                    if (reader.TokenType == JsonTokenType.EndArray)
+                    {
+                        return list;
+                    }
+
+                    list.Push(ReadValue(ref reader, options));
+                }
+
+                throw new JsonException();
+            default:
+                throw new JsonException();
+        }
+    }
+
+    public override void Write(Utf8JsonWriter writer, JsObject value, JsonSerializerOptions options)
+    {
+        writer.WriteStartObject();
+
+        foreach (var kvp in value.GetOwnProperties())
+        {
+            writer.WritePropertyName(kvp.Key.ToString());
+            WriteValue(writer, kvp.Value, options);
+        }
+
+        writer.WriteEndObject();
+    }
+
+    private void WriteValue(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
+    {
+        switch (value)
+        {
+            case string s:
+                writer.WriteStringValue(s);
+                break;
+            case long l:
+                writer.WriteNumberValue(l);
+                break;
+            case double d:
+                writer.WriteNumberValue(d);
+                break;
+            case bool b:
+                writer.WriteBooleanValue(b);
+                break;
+            case null:
+                writer.WriteNullValue();
+                break;
+            case Dictionary<string, object> dict:
+                writer.WriteStartObject();
+                foreach (var kvp in dict)
+                {
+                    writer.WritePropertyName(kvp.Key);
+                    WriteValue(writer, kvp.Value, options);
+                }
+
+                writer.WriteEndObject();
+                break;
+            case List<object> list:
+                writer.WriteStartArray();
+                foreach (var item in list)
+                {
+                    WriteValue(writer, item, options);
+                }
+
+                writer.WriteEndArray();
+                break;
+            case JsonNode node:
+                JsonSerializer.Serialize(writer, node, options);
+                break;
+            default:
+                throw new InvalidOperationException($"Unsupported type: {value?.GetType()}");
+        }
+    }
+}
+
+public static class JsonDefaults
+{
+    public const int DictionaryCapacity = 4;
+
+    public static JsonSerializerOptions JsonSerializerOptions { get; }
+
+    static JsonDefaults()
+    {
+        var options = new JsonSerializerOptions();
+        options.Converters.Add(new DictionaryStringObjectJsonConverter());
+        options.Converters.Add(new NativeJsValueJsonConverter());
+
+        JsonSerializerOptions = options;
+    }
+}
+
+public enum TestDataType
+{
+    ClrObject,
+    JsonNode,
+    Dictionary,
+    JsValue
+}

+ 12 - 3
Jint/Extensions/ReflectionExtensions.cs

@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
@@ -47,10 +48,18 @@ internal static class ReflectionExtensions
             .Where(static m => m.IsExtensionMethod());
     }
 
-    internal static IEnumerable<MethodInfo> GetOperatorOverloadMethods([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type)
+    private static readonly ConcurrentDictionary<Type, List<MethodInfo>> _operatorOverloadMethodCache = new();
+
+    internal static List<MethodInfo> GetOperatorOverloadMethods([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] this Type type)
     {
-        return type.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
-            .Where(static m => m.IsSpecialName);
+        return _operatorOverloadMethodCache.GetOrAdd(type, static t =>
+        {
+#pragma warning disable IL2070
+            return t.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
+                .Where(static m => m.IsSpecialName)
+                .ToList();
+#pragma warning restore IL2070
+        });
     }
 
     private static bool IsExtensionMethod(this MethodBase methodInfo)

+ 7 - 3
Jint/Native/Function/Function.cs

@@ -57,9 +57,8 @@ public abstract partial class Function : ObjectInstance, ICallable
         Engine engine,
         Realm realm,
         JsString? name,
-        FunctionThisMode thisMode = FunctionThisMode.Global,
-        ObjectClass objectClass = ObjectClass.Function)
-        : base(engine, objectClass)
+        FunctionThisMode thisMode = FunctionThisMode.Global)
+        : base(engine, ObjectClass.Function)
     {
         if (name is not null)
         {
@@ -366,6 +365,11 @@ public abstract partial class Function : ObjectInstance, ICallable
     // native syntax doesn't expect to have private identifier indicator
     private static readonly char[] _functionNameTrimStartChars = ['#'];
 
+    public sealed override object ToObject()
+    {
+        return (JsCallDelegate) Call;
+    }
+
     public override string ToString()
     {
         if (_functionDefinition?.Function is Node node && _engine.Options.Host.FunctionToStringHandler(this, node) is { } s)

+ 5 - 3
Jint/Runtime/Interop/DefaultObjectConverter.cs

@@ -160,7 +160,7 @@ internal static class DefaultObjectConverter
     }
 
 #if NET8_0_OR_GREATER
-    private static JsValue? ConvertSystemTextJsonValue(Engine engine, System.Text.Json.Nodes.JsonValue value)
+    private static JsValue? ConvertSystemTextJsonValue(Engine engine, System.Text.Json.Nodes.JsonNode value)
     {
         return value.GetValueKind() switch
         {
@@ -168,13 +168,15 @@ internal static class DefaultObjectConverter
             System.Text.Json.JsonValueKind.Array => JsValue.FromObject(engine, value),
             System.Text.Json.JsonValueKind.String => JsString.Create(value.ToString()),
 #pragma warning disable IL2026, IL3050
-            System.Text.Json.JsonValueKind.Number => value.TryGetValue<int>(out var intValue) ? JsNumber.Create(intValue) : System.Text.Json.JsonSerializer.Deserialize<double>(value),
+            System.Text.Json.JsonValueKind.Number => ((System.Text.Json.Nodes.JsonValue) value).TryGetValue<int>(out var intValue)
+                ? JsNumber.Create(intValue)
+                : System.Text.Json.JsonSerializer.Deserialize<double>(value),
 #pragma warning restore IL2026, IL3050
             System.Text.Json.JsonValueKind.True => JsBoolean.True,
             System.Text.Json.JsonValueKind.False => JsBoolean.False,
             System.Text.Json.JsonValueKind.Undefined => JsValue.Undefined,
             System.Text.Json.JsonValueKind.Null => JsValue.Null,
-            _ => null
+            _ => null,
         };
     }
 #endif

+ 11 - 31
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Linq.Expressions;
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using Jint.Extensions;
 using Jint.Native;
 using Jint.Native.Function;
@@ -28,11 +29,10 @@ public class DefaultTypeConverter : ITypeConverter
     private static readonly ConcurrentDictionary<TypeConversionKey, MethodInfo?> _knownCastOperators = new();
 
     private static readonly Type intType = typeof(int);
-    private static readonly Type iCallableType = typeof(Func<JsValue, JsValue[], JsValue>);
+    private static readonly Type iCallableType = typeof(JsCallDelegate);
     private static readonly Type jsValueType = typeof(JsValue);
     private static readonly Type objectType = typeof(object);
     private static readonly Type engineType = typeof(Engine);
-    private static readonly Type typeType = typeof(Type);
 
     private static readonly MethodInfo changeTypeIfConvertible = typeof(DefaultTypeConverter).GetMethod(
         "ChangeTypeOnlyIfConvertible", BindingFlags.NonPublic | BindingFlags.Static)!;
@@ -66,6 +66,8 @@ public class DefaultTypeConverter : ITypeConverter
         return TryConvert(value, type, formatProvider, propagateException: false, out converted, out _);
     }
 
+    private static readonly ConditionalWeakTable<IFunction, Delegate> _delegateCache = new();
+
     private bool TryConvert(
         object? value,
         [DynamicallyAccessedMembers(InteropHelper.DefaultDynamicallyAccessedMemberTypes)] Type type,
@@ -129,19 +131,11 @@ public class DefaultTypeConverter : ITypeConverter
         {
             if (typeof(Delegate).IsAssignableFrom(type) && !type.IsAbstract)
             {
-                // use target function instance as cache holder, this way delegate and target hold same lifetime
-                var delegatePropertyKey = "__jint_delegate_" + type.GUID;
-
-                var func = (Func<JsValue, JsValue[], JsValue>) value;
-                var functionInstance = func.Target as Function;
-
-                var d = functionInstance?.GetHiddenClrObjectProperty(delegatePropertyKey) as Delegate;
-
-                if (d is null)
-                {
-                    d = BuildDelegate(type, func);
-                    functionInstance?.SetHiddenClrObjectProperty(delegatePropertyKey, d);
-                }
+                var func = (JsCallDelegate) value;
+                var astFunction = (func.Target as Function)?._functionDefinition?.Function;
+                var d = astFunction is not null
+                    ? _delegateCache.GetValue(astFunction, _ => BuildDelegate(type, func))
+                    : BuildDelegate(type, func);
 
                 converted = d;
                 return true;
@@ -150,8 +144,7 @@ public class DefaultTypeConverter : ITypeConverter
 
         if (type.IsArray)
         {
-            var source = value as object[];
-            if (source == null)
+            if (value is not object[] source)
             {
                 problemMessage = $"Value of object[] type is expected, but actual type is {value.GetType()}";
                 return false;
@@ -268,7 +261,7 @@ public class DefaultTypeConverter : ITypeConverter
 
     private Delegate BuildDelegate(
         [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type,
-        Func<JsValue, JsValue[], JsValue> function)
+        JsCallDelegate function)
     {
         var method = type.GetMethod("Invoke");
         var arguments = method!.GetParameters();
@@ -397,16 +390,3 @@ public class DefaultTypeConverter : ITypeConverter
     }
 
 }
-
-internal static class ObjectExtensions
-{
-    public static object? GetHiddenClrObjectProperty(this ObjectInstance obj, string name)
-    {
-        return (obj.Get(name) as IObjectWrapper)?.Target;
-    }
-
-    public static void SetHiddenClrObjectProperty(this ObjectInstance obj, string name, object value)
-    {
-        obj.SetOwnProperty(name, new PropertyDescriptor(ObjectWrapper.Create(obj.Engine, value), PropertyFlag.AllForbidden));
-    }
-}

+ 52 - 28
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -1,4 +1,5 @@
 using System.Collections;
+using System.Collections.Concurrent;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Reflection;
@@ -74,48 +75,56 @@ public class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable<ObjectWr
         return new ObjectWrapper(engine, target, type);
     }
 
+    private static readonly ConcurrentDictionary<Type, Type?> _arrayLikeWrapperResolution = new();
+
     private static bool TryBuildArrayLikeWrapper(
         Engine engine,
         object target,
         [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type,
         [NotNullWhen(true)] out ArrayLikeWrapper? result)
     {
-#pragma warning disable IL2055
-#pragma warning disable IL3050
-
         result = null;
 
-        // check for generic interfaces
-        foreach (var i in type.GetInterfaces())
+        var arrayWrapperType = _arrayLikeWrapperResolution.GetOrAdd(type, static t =>
         {
-            if (!i.IsGenericType)
+#pragma warning disable IL2055
+#pragma warning disable IL2070
+#pragma warning disable IL3050
+
+            // check for generic interfaces
+            foreach (var i in t.GetInterfaces())
             {
-                continue;
-            }
+                if (!i.IsGenericType)
+                {
+                    continue;
+                }
 
-            var arrayItemType = i.GenericTypeArguments[0];
+                var arrayItemType = i.GenericTypeArguments[0];
 
-            if (i.GetGenericTypeDefinition() == typeof(IList<>))
-            {
-                var arrayWrapperType = typeof(GenericListWrapper<>).MakeGenericType(arrayItemType);
-                result = (ArrayLikeWrapper) Activator.CreateInstance(arrayWrapperType, engine, target, type)!;
-                break;
-            }
+                if (i.GetGenericTypeDefinition() == typeof(IList<>))
+                {
+                    return typeof(GenericListWrapper<>).MakeGenericType(arrayItemType);
+                }
 
-            if (i.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))
-            {
-                var arrayWrapperType = typeof(ReadOnlyListWrapper<>).MakeGenericType(arrayItemType);
-                result = (ArrayLikeWrapper) Activator.CreateInstance(arrayWrapperType, engine, target, type)!;
-                break;
+                if (i.GetGenericTypeDefinition() == typeof(IReadOnlyList<>))
+                {
+                    return typeof(ReadOnlyListWrapper<>).MakeGenericType(arrayItemType);
+                }
             }
-        }
-
 #pragma warning restore IL3050
+#pragma warning restore IL2070
 #pragma warning restore IL2055
 
-        // least specific
-        if (result is null && target is IList list)
+            return null;
+        });
+
+        if (arrayWrapperType is not null)
+        {
+            result = (ArrayLikeWrapper) Activator.CreateInstance(arrayWrapperType, engine, target, type)!;
+        }
+        else if (target is IList list)
         {
+            // least specific
             result = new ListWrapper(engine, list, type);
         }
 
@@ -197,13 +206,28 @@ public class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable<ObjectWr
 
     public override JsValue Get(JsValue property, JsValue receiver)
     {
-        if (!_typeDescriptor.IsDictionary
-            && Target is ICollection c
-            && CommonProperties.Length.Equals(property))
+        // check fast path before producing properties
+        if (ReferenceEquals(receiver, this) && property.IsString())
         {
-            return JsNumber.Create(c.Count);
+            // try some fast paths
+            if (!_typeDescriptor.IsDictionary)
+            {
+                if (Target is ICollection c && CommonProperties.Length.Equals(property))
+                {
+                    return JsNumber.Create(c.Count);
+                }
+            }
+            else
+            {
+                if (_typeDescriptor.IsStringKeyedGenericDictionary
+                    && _typeDescriptor.TryGetValue(Target, property.ToString(), out var value))
+                {
+                    return FromObject(_engine, value);
+                }
+            }
         }
 
+        // slow path requires us to create a property descriptor that might get cached or not
         var desc = GetOwnProperty(property, mustBeReadable: true, mustBeWritable: false);
         if (desc != PropertyDescriptor.Undefined)
         {