Tuesday, February 19, 2013

Memorizing method results with PostSharp (part 3)

This is the 3rd episode of a series of posts on how to cache statically method results.
In the first post we discussed what is postsharp, and how can it be used to create an attribute that will cache the results.
In the second post we removed hard references to the ibject instance, and to the arguments.
In this third post, we will focus on the missing limitations, namely generics and null parameters.
Lets jump right into it then.

Generic methods

Well, to make sure that generic methods do not mess up the picture, lets create a generic method inside our TestClass:
        public class TestClass
        {
            // previous code
            [...]

            public Dictionary<Type, int> NrOfGenericExecutions = new Dictionary<Type,int>();
            [CacheResultAttribute.CacheResult(CacheLocations.Static)]
            public int DoubleTheNumber<T>(int i)
            {
                if (!NrOfGenericExecutions.ContainsKey(typeof(T)))
                {
                    NrOfGenericExecutions.Add(typeof(T), 0);
                }
                NrOfGenericExecutions[typeof(T)] = NrOfGenericExecutions[typeof(T)] + 1;
                return i * 2;
            }
        }
and then write a test for it:
        [TestMethod]
        public void TestGenericMethods()
        {
            var t = new TestClass();
            Assert.AreEqual(0, t.NrOfGenericExecutions.Count);
            t.DoubleTheNumber<int>(2);
            Assert.AreEqual(1, t.NrOfGenericExecutions.Count);
            Assert.AreEqual(1, t.NrOfGenericExecutions[typeof(int)]);
            t.DoubleTheNumber<int>(3);
            Assert.AreEqual(1, t.NrOfGenericExecutions.Count);
            Assert.AreEqual(2, t.NrOfGenericExecutions[typeof(int)]);
            t.DoubleTheNumber<int>(2);
            Assert.AreEqual(1, t.NrOfGenericExecutions.Count);
            Assert.AreEqual(2, t.NrOfGenericExecutions[typeof(int)]);

            t.DoubleTheNumber<string>(2);
            Assert.AreEqual(2, t.NrOfGenericExecutions.Count);
            Assert.AreEqual(2, t.NrOfGenericExecutions[typeof(int)]);
            Assert.AreEqual(1, t.NrOfGenericExecutions[typeof(string)]);
            t.DoubleTheNumber<string>(3);
            Assert.AreEqual(2, t.NrOfGenericExecutions.Count);
            Assert.AreEqual(2, t.NrOfGenericExecutions[typeof(int)]);
            Assert.AreEqual(2, t.NrOfGenericExecutions[typeof(string)]);
            t.DoubleTheNumber<string>(2);
            Assert.AreEqual(2, t.NrOfGenericExecutions.Count);
            Assert.AreEqual(2, t.NrOfGenericExecutions[typeof(int)]);
            Assert.AreEqual(2, t.NrOfGenericExecutions[typeof(string)]);
        }
Now running this test will fail. Why is that? well, since we are having an instance attribute, i guess the attribute is the same for all methods of DoubleTheNumber<T>. So all we need to do here, is save the results in a dictionary, that has the method definition as its base. The changes are minimal. We change our methodCache private field to
        private IDictionary<MethodBase, object> methodCache = new Dictionary<MethodBase, object>();
of course we need to initialize this field for new instances, so the implementaion of IInstanceScopedAspect changes with the initialization of this field:
        void IInstanceScopedAspect.RuntimeInitializeInstance()
        {
            this.methodCache = new Dictionary<MethodBase, object>();
        }
Then we change our onEntry and onExit methods, to use the dictionary value according to the exact method definition, instead of just a simple object. But everything else stays the same
        public override void OnEntry(PostSharp.Aspects.MethodExecutionArgs args)
        {

            if (!this.methodCache.ContainsKey(args.Method))
            {
                this.methodCache.Add(args.Method, null);
            }

            //remaining initialization
            [...]
            object resultDictionary = this.methodCache[args.Method];
            [...]

        }
And voila, our tests pass. Great. Generic arguments nailed.
Now what about null value arguments?

null values as arguments

So do we support null values? Lets see in a test. We are using here our TestClass2 from the 2nd post.
        [TestMethod]
        public void TestNullArguments()
        {
            var t = new TestClass2();
            var ta = new TestClass();
            Assert.AreEqual(0, t.NrOfExecutions);
            t.DoSomething(ta);
            Assert.AreEqual(1, t.NrOfExecutions);
            t.DoSomething(ta);
            Assert.AreEqual(1, t.NrOfExecutions);
            t.DoSomething(null);
            Assert.AreEqual(2, t.NrOfExecutions);
            t.DoSomething(null);
            Assert.AreEqual(2, t.NrOfExecutions);
            t.DoSomething(ta);
            Assert.AreEqual(2, t.NrOfExecutions);
            
        }
Well, this test fails miserably. The ConditionalWeakTable will throw an exception, if you try to add a null key. Actually if you think about it, it kinda misses the point. The structure is there, so if the key object is garbage collected, so the whole entry will disappear. Now since null cannot be garbage collected...
So it seems that we will need an additional way of storing values for nulls. But is that so difficult? We already differentiate arguments that can be null (IsValueType cannot be null), so lets just store the values for atguments like that in a bit extended structure. We create a wrapper around ConditionalWeakTable. I dont want to implement any special methods, and i will just expose the fields directly, but you could even try to implement IDictionary, or just pull some of the methods from the internal field to the object itself, its up to you. I just did:
    public class WeakNullableKeyValueCollection<TKey, TValue>
        where TKey: class
        where TValue : class
    {
        /// <summary>
        /// the table for the keys
        /// </summary>
        public ConditionalWeakTable<TKey, TValue> ConditionalWeakTable { get; private set; }

        private TValue _ValueForNull;
        /// <summary>
        /// the value for null
        /// </summary>
        public TValue ValueForNull { get { return _ValueForNull; } set { _ValueForNull = value; IsNullValueSet = true; } }

        /// <summary>
        /// whether the null value is set
        /// </summary>
        public bool IsNullValueSet { get; private set; }


        public WeakNullableKeyValueCollection()
        {
            ConditionalWeakTable = new ConditionalWeakTable<TKey, TValue>();
        }
    }
Using this class instead of simply a ConditionalWeakTable class should do the trick:
        public override void OnEntry(PostSharp.Aspects.MethodExecutionArgs args)
        {
            //existing code
            [...]
                        if (!types[i].IsValueType)
                        {
                            if (arg == null)
                            {
                                if ((resultDictionary as WeakNullableKeyValueCollection<object, object>).IsNullValueSet)
                                {
                                    resultDictionary = (resultDictionary as WeakNullableKeyValueCollection<object, object>).ValueForNull;
                                }
                                else
                                {
                                    isMissingKey = true;
                                    break;
                                }
                            }
                            else
                            {
                                if ((resultDictionary as WeakNullableKeyValueCollection<object, object>).ConditionalWeakTable.TryGetValue(arg, out result))
                                {
                                    resultDictionary = result;
                                }
                                else
                                {
                                    isMissingKey = true;
                                    break;
                                }

                            }
                        }
            //existing code
            [...]
        }

        public override void OnExit(PostSharp.Aspects.MethodExecutionArgs args)
        {
            //existing code
            [...]
                    if (!types[i].IsValueType)
                    {
                        if (getter() == null)
                        {
                            setter(new WeakNullableKeyValueCollection<object, object>());
                        }
                        var currenttable = getter();

                        getter = () =>
                        {
                            if (arg == null)
                            {
                                return (currenttable as WeakNullableKeyValueCollection<object, object>).ValueForNull;
                            }
                            else
                            {
                                object temp;
                                (currenttable as WeakNullableKeyValueCollection<object, object>).ConditionalWeakTable.TryGetValue(arg, out temp);
                                return temp;
                            }
                        };
                        setter = o =>
                        {
                            if (arg == null)
                            {
                                (currenttable as WeakNullableKeyValueCollection<object, object>).ValueForNull = o;
                            }
                            else
                            {
                                (currenttable as WeakNullableKeyValueCollection<object, object>).ConditionalWeakTable.Add(arg, o);
                            }
                        };

                    }
            //existing code
            [...]
        }
Now run the unit tests, and we get them all passed. Cool null support added.

Generic classes

Now what about generic classes? Lets write a small testclass,
        public class TestClass<T>
        {
            public static int StaticNrOfExecutions = 0;
            [CacheResultAttribute.CacheResult(CacheLocations.Static)]
            public static int StaticDoubleTheNumber(int i)
            {
                StaticNrOfExecutions++;
                return i * 2;
            }

            public int CallStaticDoubleTheNumber(int i)
            {
                return StaticDoubleTheNumber(i);
            }


            public int NrOfExecutions = 0;
            [CacheResultAttribute.CacheResult(CacheLocations.Static)]
            public int DoubleTheNumber(int i)
            {
                NrOfExecutions++;
                return i * 2;
            }
        }
and a test for it:
        [TestMethod]
        public void TestGenericClasses()
        {
            var ti = new TestClass<int>();
            var ts = new TestClass<string>();
            Assert.AreEqual(0, ti.NrOfExecutions);
            Assert.AreEqual(0, TestClass<int>.StaticNrOfExecutions);
            Assert.AreEqual(0, ts.NrOfExecutions);
            Assert.AreEqual(0, TestClass<string>.StaticNrOfExecutions);

            ti.DoubleTheNumber(1);
            Assert.AreEqual(1, ti.NrOfExecutions);
            Assert.AreEqual(0, TestClass<int>.StaticNrOfExecutions);
            Assert.AreEqual(0, ts.NrOfExecutions);
            Assert.AreEqual(0, TestClass<string>.StaticNrOfExecutions);

            ti.CallStaticDoubleTheNumber(1);
            Assert.AreEqual(1, ti.NrOfExecutions);
            Assert.AreEqual(1, TestClass<int>.StaticNrOfExecutions);
            Assert.AreEqual(0, ts.NrOfExecutions);
            Assert.AreEqual(0, TestClass<string>.StaticNrOfExecutions);

            TestClass<int>.StaticDoubleTheNumber(1);
            Assert.AreEqual(1, ti.NrOfExecutions);
            Assert.AreEqual(1, TestClass<int>.StaticNrOfExecutions);
            Assert.AreEqual(0, ts.NrOfExecutions);
            Assert.AreEqual(0, TestClass<string>.StaticNrOfExecutions);

            TestClass<string>.StaticDoubleTheNumber(1);
            Assert.AreEqual(1, ti.NrOfExecutions);
            Assert.AreEqual(1, TestClass<int>.StaticNrOfExecutions);
            Assert.AreEqual(0, ts.NrOfExecutions);
            Assert.AreEqual(1, TestClass<string>.StaticNrOfExecutions);

            ts.CallStaticDoubleTheNumber(1);
            Assert.AreEqual(1, ti.NrOfExecutions);
            Assert.AreEqual(1, TestClass<int>.StaticNrOfExecutions);
            Assert.AreEqual(0, ts.NrOfExecutions);
            Assert.AreEqual(1, TestClass<string>.StaticNrOfExecutions);

            ts.DoubleTheNumber(1);
            Assert.AreEqual(1, ti.NrOfExecutions);
            Assert.AreEqual(1, TestClass<int>.StaticNrOfExecutions);
            Assert.AreEqual(1, ts.NrOfExecutions);
            Assert.AreEqual(1, TestClass<string>.StaticNrOfExecutions);
        }
Well running the test passes, so it seems that somewhere along the way we already implemented this feature.
Actually the reason why we dont get collisions, is that the MethodBase, that we use as a key for method return value caching, is different between 2 generic classes with different Generic parameter. So we dont run into any collision.

Well, this concludes the functionality we set off for. The source code can be downloaded here
Please dont forget about the limitations mentioned in the second post, that were not addressed in this one.
If you have any comments or notes, please feel free to add them.
Have a nice day!

No comments:

Post a Comment