MethodCaller: Possible Big Performance Boost??

MethodCaller: Possible Big Performance Boost??

Old forum URL: forums.lhotka.net/forums/t/4101.aspx


vdhant posted on Sunday, December 30, 2007

Hi guys

Of late i have been getting right down into CLSA and looking at exactly what makes it up and how it works. As a result of this it is quite evident that MethodCaller get quite a work out and if this process could be refined a little further maybe it might help a fair bit. Hence I came up with the following. PLEASE NOTE: you will notice that the structure has changed a little to allow for the caching and an extra try catch statement has been added to prevent a posible AmbiguousMatchException that could occur but in essence the rest is the same. Also just for testing purposes I have not included some of the Resources calls and some of the other methods. Also this has NOT been fully tested, due to my current commitments I only got it compiling for the moment, but i wanted to see what others thought.
Thanks
Anthony

public static class MethodHelper
{
    #region Private Fields
    private static readonly Dictionary<string, MethodInfo> _GlobalMethodInfoCount = new Dictionary<string, MethodInfo>();
    private static readonly Dictionary<string, Dictionary<Type[], MethodInfo>> _GlobalMethodInfoType = new Dictionary<string, Dictionary<Type[], MethodInfo>>();
    private static readonly Dictionary<string, MethodInfo> _GlobalMethodInfoTest = new Dictionary<string, MethodInfo>();

    private const BindingFlags _AllLevelFlags = BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    private const BindingFlags _OneLevelFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    #endregion

    #region Private Methods

    #region General
    private static Type GetObjectType(object criteria)
    {
        return criteria.GetType().DeclaringType;
    }
    #endregion

    #region Derive Method By
    private static MethodInfo DeriveMethodByLoopCount(MethodInfo[] methods, string methodName, int currentParamLength)
    {
        MethodInfo result = null;
        foreach (MethodInfo method in methods)
        {
            if (method.Name == methodName && method.GetParameters().Length == currentParamLength)
            {
                result = method;
                break;
            }
        }
        return result;
    }

    private static MethodInfo DeriveMethodByTest(Type objectType, string methodName, params object[] parameters)
    {
        MethodInfo result = null;
        //It is posible for more than one result to be found
        try
        {
            result = objectType.GetMethod(methodName, MethodHelper._AllLevelFlags);
        }
        catch (AmbiguousMatchException)
        {
            result = MethodHelper.DeriveMethodByLoopCount(objectType.GetMethods(), methodName, parameters.Length);
            if (result == null)
                throw;
        }
        return result;
    }

    private static MethodInfo DeriveMethodByType(Type currentObjectType, string methodName, Type[] paramTypes)
    {
        //Finds the relevent method
        MethodInfo result = null;
        do
        {
            //Find for a strongly typed match
            result = currentObjectType.GetMethod(methodName, MethodHelper._OneLevelFlags, null, paramTypes, null);
            if (result != null)
                break;  //Match found

            currentObjectType = currentObjectType.BaseType;
        } while (currentObjectType != null);

        return result;
    }

    private static MethodInfo DeriveMethodByCount(Type currentObjectType, string methodName, int parameterCount)
    {
        //Walk up the inheritance hierarchy looking for a method with the right number of parameters
        MethodInfo result = null;
        Type currentType = currentObjectType;
        do
        {
            //It is posible for more than one result to be found
            try
            {
                result = currentType.GetMethod(methodName, MethodHelper._OneLevelFlags);
                if (result != null && result.GetParameters().Length == parameterCount)
                    break;  //Match found
            }
            catch (AmbiguousMatchException)
            {
                result = MethodHelper.DeriveMethodByLoopCount(currentObjectType.GetMethods(MethodHelper._OneLevelFlags), methodName, parameterCount);
                if (result == null)
                    throw;
            }

            currentType = currentType.BaseType;
        } while (currentType != null);

        return result;
    }
    #endregion

    #region Retreive Cached Method By
    private static MethodInfo RetreiveCachedMethodByType(Type currentObjectType, string methodName, Type[] paramTypes)
    {
        string fullMethodName = currentObjectType.GetType().FullName + "." + methodName + "();";
        bool containsMethodKey = false;

        //Brings back the correct methodName for the type if it exists
        if (MethodHelper._GlobalMethodInfoType.ContainsKey(fullMethodName))
        {
            if (MethodHelper._GlobalMethodInfoType[fullMethodName].ContainsKey(paramTypes))
                return MethodHelper._GlobalMethodInfoType[fullMethodName][paramTypes];
            containsMethodKey = true;
        }

        //Pulls out the dictionary for the current method if it exists
        Dictionary<Type[], MethodInfo> methodDictionary;
        if (containsMethodKey)
            methodDictionary = MethodHelper._GlobalMethodInfoType[fullMethodName];
        else
        {
            methodDictionary = new Dictionary<Type[], MethodInfo>();
            MethodHelper._GlobalMethodInfoType.Add(fullMethodName, methodDictionary);
        }

        //Gets method out if it can be found
        MethodInfo result = MethodHelper.DeriveMethodByType(currentObjectType, methodName, paramTypes);

        //Adds in the methodName info into the dictionary caching it
        methodDictionary.Add(paramTypes, result);

        return result;
    }

    private static MethodInfo RetreiveCachedMethodByCount(Type currentObjectType, string methodName, int parameterCount)
    {
        string fullMethodName = currentObjectType.GetType().FullName + "." + methodName + "()_" + parameterCount + ";";
        if (MethodHelper._GlobalMethodInfoCount.ContainsKey(fullMethodName))
            return MethodHelper._GlobalMethodInfoCount[fullMethodName];

        //Gets method out if it can be found
        MethodInfo result = MethodHelper.DeriveMethodByCount(currentObjectType, methodName, parameterCount);

        //Adds in the methodName info into the dictionary caching it
        MethodHelper._GlobalMethodInfoCount.Add(fullMethodName, result);

        return result;
    }

    private static MethodInfo RetreiveCachedMethodByTest(Type currentObjectType, string methodName, params object[] parameters)
    {
        string fullMethodName = currentObjectType.GetType().FullName + "." + methodName + "();";
        if (MethodHelper._GlobalMethodInfoTest.ContainsKey(fullMethodName))
            return MethodHelper._GlobalMethodInfoTest[fullMethodName];

        //Gets method out if it can be found
        MethodInfo result = MethodHelper.DeriveMethodByTest(currentObjectType, methodName, parameters);

        //Adds in the methodName info into the dictionary caching it
        MethodHelper._GlobalMethodInfoTest.Add(fullMethodName, result);

        return result;
    }
    #endregion

    #endregion

    #region Public Methods
    public static object CallMethodIfImplemented(object currentObject, string methodName, params object[] parameters)
    {
        MethodInfo info = GetMethod(currentObject.GetType(), methodName, parameters);

        if (info != null)
            return CallMethod(currentObject, info, parameters);
        else
            return null;
    }

    public static object CallMethod(object currentObject, string methodName, params object[] parameters)
    {
        MethodInfo info = GetMethod(currentObject.GetType(), methodName, parameters);

        if (info == null)
            throw new NotImplementedException(methodName + " Method Not Implemented");

        return CallMethod(currentObject, info, parameters);
    }

    public static object CallMethod(object currentObject, MethodInfo info, params object[] parameters)
    {
        //Call a private methodName on the object
        object result;
        try
        {
            result = info.Invoke(currentObject, parameters);
        }
        catch (Exception ex)
        {
            throw new CallMethodException(info.Name + " Method Call Failed", ex.InnerException);
        }
        return result;
    }

    public static MethodInfo GetMethod(Type objectType, string methodName, params object[] parameters)
    {
        MethodInfo result = null;

        //Try to find a strongly typed match
        result = MethodHelper.FindMethodByType(objectType, methodName, TypeHelper.GetParameterTypes(parameters));

        //With the right number of parameters
        if (result == null)
            result = MethodHelper.FindMethodByCount(objectType, methodName, parameters.Length);

        //No strongly typed match found, get default
        if (result == null)
            result = MethodHelper.FindMethodByTest(objectType, methodName, parameters.Length);

        return result;
    }

    public static MethodInfo FindMethodByType(Type currentObjectType, string methodName, Type[] paramTypes)
    {
        return MethodHelper.RetreiveCachedMethodByType(currentObjectType, methodName, paramTypes);
    }

    public static MethodInfo FindMethodByCount(Type currentObjectType, string methodName, int parameterCount)
    {
        return MethodHelper.RetreiveCachedMethodByCount(currentObjectType, methodName, parameterCount);
    }

    public static MethodInfo FindMethodByTest(Type currentObjectType, string methodName, params object[] parameters)
    {
        return MethodHelper.RetreiveCachedMethodByTest(currentObjectType, methodName, parameters);
    }
    #endregion
}

rasupit replied on Sunday, December 30, 2007

Anthony,
I agree in essence with your approach that the method info's need to be cached to improve performance.

I actually had the pleasure figuring out a bug in MethodCaller.GetMethod method and while looking at the code and found the way it resolve an ambiguous match by caching for AmbiguousMatchException could be improved.  However at that time, I did not have any idea how to improve it therefore I did not make any suggestion.

Recently, I have been looking at LinFu Framework (there are several articles on CodeProject posted by the author Philip Laureano, here is the first article http://www.codeproject.com/KB/cs/LinFuPart1.aspx) and found his MethodFinder class is very elegant. His method is to use FussyFinder to find the matching method and applying weight to match types (method name, return type, parameters, etc) through PredicateBuilder.

Ricky Supit.

vdhant replied on Monday, December 31, 2007

Hi Ricky
The LinFu framework sounds interesting enough and I particularly like the idea of the FussyFinder to help find the best match, but I guess it is a fairly big deviartion from what the methodcaller currently does. Hence I was trying to fix that bug I mentioned and implement the caching.

The only thing I did find after using it a little is that the after using it and doing a little bit more testing the Dictionary<string, Dictionary<Type[], MethodInfo>> doesn't quite work the wway I had hoped and hence it probably needs to change to something like this.
private static readonly Dictionary<string, Dictionary<string, MethodInfo>> _GlobalMethodInfoType = new Dictionary<string, Dictionary<string, MethodInfo>>();

private static string ConvertToString(Type[] types)
{
    StringBuilder format = new StringBuilder(";");
    foreach (Type type in types)
        format.Append(type.FullName +
";");
    return format.ToString();
}

private static MethodInfo RetreiveCachedMethodByType(Type currentObjectType, string methodName, Type[] paramTypes)
{
    string fullMethodName = currentObjectType.GetType().FullName + "." + methodName + "();";
    bool containsMethodKey = false;
    string paramTypesString = MethodHelper.ConvertToString(paramTypes);

    //Brings back the correct methodName for the type if it exists
    if (MethodHelper._GlobalMethodInfoType.ContainsKey(fullMethodName))
    {
        if (MethodHelper._GlobalMethodInfoType[fullMethodName].ContainsKey(paramTypesString))
            return MethodHelper._GlobalMethodInfoType[fullMethodName][paramTypesString];
        containsMethodKey =
true;
    }

    //Pulls out the dictionary for the current method if it exists
    Dictionary<string, MethodInfo> methodDictionary;
    if (containsMethodKey)
        methodDictionary =
MethodHelper._GlobalMethodInfoType[fullMethodName];
    else
    {
        methodDictionary =
new Dictionary<string, MethodInfo>();
        MethodHelper._GlobalMethodInfoType.Add(fullMethodName, methodDictionary);
    }

    //Gets method out if it can be found
    MethodInfo result = MethodHelper.DeriveMethodByType(currentObjectType, methodName, paramTypes);
    //Adds in the methodName info into the dictionary caching it
    methodDictionary.Add(paramTypesString, result);

    return result;
}

RockfordLhotka replied on Monday, January 07, 2008

Have you done comparative performance tests using both the old and new code?

ajj3085 replied on Tuesday, January 08, 2008

There's another possiblity for speeding up reflection base method calls.  I actually implemented it in my DAL when I had to add some more MethodInfo calls.  The technique can be found here.  What you do is basically do the reflection once, then compile the delegate and store it in a static field. 

I haven't done any testing, but after I added the feature that required the MethodInfo call, my unit tests ran noticibly slower.  I'm talking from a few seconds to about a minute and a half to run all the tests.  Implementing the method in the link above brought performance back to the few second range. 

If you like, I can try this in Csla.  Just tell me how you would like things timed.  I can't promise I'll get to it soon though.  Smile [:)]

Andy

RockfordLhotka replied on Tuesday, January 08, 2008

Thanks Andy, that is an interesting article.

After spending yesterday (and last night) messing with MethodCaller to get it to support ParamArray/params parameters on target methods, I am left wondering if the technique in the article will work in the general case.

I now fully appreciate just how cool the VB runtime's late binding support really is. I knew it was cool, but given all the nasty edge cases (which I don't solve btw), they obviously have some impressive code there...

I had thought about doing something like this for 3.5, but now I don't know. I don't know that I have another 16-20 hours of time to sink into that one class... I appreciate your offer of help, and (if you'll sign the contributor agreement) will accept it gladly! My deadline for 3.5 to be feature complete is Jan 25 though, so time is somewhat short I'm afraid.

 

Someone in the thread mentioned that they'd fixed a bug in MethodCaller? Can you be specific? In particular, what does the target method signature look like and what is passed from the caller?

vdhant replied on Tuesday, January 08, 2008

Hi Rocky,
That was me who said that i think i have found a potential bug.

In the public static MethodInfo FindMethod(Type objType, string method, int parameterCount) method, it does a loop looking for methods that have a set number of paramerters. Within the loop it uses this MethodInfo info = currentType.GetMethod(method, oneLevelFlags); line which calls the GetMethod method. As far as i can see anywhere the GetMethod is used it has the ability to throw an AmbiguousMatchException exectipion. Now when you are doing calling this public static MethodInfo FindMethod(Type objType, string method, Type[] types) method, you should never get this because you can't compile with two methods of the same signature, but with the count version there is the possibility that it could. Hence why i thought that this is a bug and should have a try catch around it. You will see in code that i provided that it checks for this case and it the exception occurs runs the pretty much the same code that you have in the only AmbiguousMatchException expectation check at the moment. It probably has not arisen as a bug in many cases thus far because i imagine that the first findmethod would pick up most cases.

As far as a performance testing goes, I don’t really have the setup to be able to give you figures on that one, but what i do know is that there is a lot of looping that occurs to find the correct method and this looping logic, given a type, method name and a set of params is always going to produce the same result. Hence is there need to conduct that looping and reflection every time. Hence why i was trying to cache the results. I guess you now have a dictionary lookup and a lock to deal with if the method isn't found in that type but for the vast majority of cases i would imagine that there would have to be some tangible performance improvement.

Anthony

Note, the version of the code that i provided doesn't contain locks where it probably should. So if you want a version with locks if you are interested just contact me.

vdhant replied on Tuesday, January 08, 2008

On another note, i took a look at the article that was mentioned and had a bit of a play around with it. Firstly it was evident was that where it gets most of its performance gain from (in terms of the figures it shows) is the fact that the delegate is being stored in a static variable. ajj3085 i am sure that you did receive a performance boost but i am guessing that you where storing the delegate in a static variable. If you had of done this same thing exact stored the methodinfo in the static variable i wonder if you would have noticed as bigger difference (which on the side is what I am proposing with the new methodcaller).

In any case it would seem that there is a performance to be had with the quoted method but when the methodinfo is cached the difference is a lot less pronounced. But that said, there is no reason why the new improved methodcaller couldn't cache both the methodinfo and the delegate if is perceived that including the delegate creation and storage provides a large enough performance boost to be included. If this is the case i thing it would be a relatively simple process to include this into the new methodcaller that i put forward. Although i don’t think i would be able to give you the final say on the performance difference, as most of the testing i have done performance wise has been hard to judge because it seems that within a cretin time period on a given thread .net does some caching on its own, so a test case would probably be have to be thought out.

Thanks
Anthony

RockfordLhotka replied on Tuesday, January 08, 2008

I don’t disagree that there should be a performance benefit – that’s the hypothesis. But the proof is always in the pudding, and time grows terribly short for anything new to change in 3.5 and I may or may not have time to do something like this before the cutoff. Time will tell.

 

Rocky

 

 

From: vdhant [mailto:cslanet@lhotka.net]
Sent: Tuesday, January 08, 2008 6:37 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] MethodCaller: Possible Big Performance Boost??

 

On another note, i took a look at the article that was mentioned and had a bit of a play around with it. Firstly it was evident was that where it gets most of its performance gain from (in terms of the figures it shows) is the fact that the delegate is being stored in a static variable. ajj3085 i am sure that you did receive a performance boost but i am guessing that you where storing the delegate in a static variable. If you had of done this same thing exact stored the methodinfo in the static variable i wonder if you would have noticed as bigger difference (which on the side is what I am proposing with the new methodcaller).

In any case it would seem that there is a performance to be had with the quoted method but when the methodinfo is cached the difference is a lot less pronounced. But that said, there is no reason why the new improved methodcaller couldn't cache both the methodinfo and the delegate if is perceived that including the delegate creation and storage provides a large enough performance boost to be included. If this is the case i thing it would be a relatively simple process to include this into the new methodcaller that i put forward. Although i don’t think i would be able to give you the final say on the performance difference, as most of the testing i have done performance wise has been hard to judge because it seems that within a cretin time period on a given thread .net does some caching on its own, so a test case would probably be have to be thought out.

Thanks
Anthony



vdhant replied on Tuesday, January 08, 2008

That’s fine. As i said off the top of my head I cannot think of practical test that would be able to isolate the difference. Because if you simply run the two through a 10000 rep loop the cached version comes out slower because of the dictionary lookups and the fact that .net caches the results on the thread as I said before. So I don’t think that a simple loop like this is a very good test. In a real situation where it is happening across multiple threads and multiple user over a period of time this is where i see the difference. But that is a lot harder to test...

In any case i just thought i would share and see what people thought.
Anthony

DavidDilworth replied on Wednesday, January 09, 2008

FWIW, here's my 2 cents.

Although I can see where you are going with this one, I'm always wary of someone claiming a "big performance boost" in this way.  I don't doubt the hypothesis, because it makes sense.  What I doubt is the investment required in coding, testing and proving the hypothesis in terms of the man-hours of development time needed.  Is it really going to give me a real "bang-for-my-buck" performance increase?  Will it reduce minutes of processing time down to just seconds?  Or are we talking about fractions of seconds being shaved off an already low number of seconds?

As performance is such a subjective subject, only a fully benchmarked solution makes any meaningful sense.  And I agree that it is not easy to see how you can benchmark this kind of stuff.  There are so many other factors that could come into play.

And are we talking theorhetical performance here, as opposed to real-life performance when the application is deployed in an operational environment and runs like a dog?

With the cost of hardware (CPUs / RAM / hard drives) being dirt cheap compared to the cost of development, the best solution to "perceived performance issues" can quite often be a hardware one, not a software one.

Don't get me wrong, if you're trying to make the best possible framework that was ever developed, then go for it and look for performance improvements everywhere.

But always remember that your customer (the business) will be more concerned that the application actually works, not how "quick" it is.  An application with no bugs that's runs "slow" and can be used in a production environment to do the job is better than an application with bugs that runs "quick" but can't be deployed.

Well that's usually true as far as business people are concerned!

ajj3085 replied on Wednesday, January 09, 2008

DavidDilworth:
And are we talking theorhetical performance here, as opposed to real-life performance when the application is deployed in an operational environment and runs like a dog?


Well, I can't say for certain it would boost performance in Csla.  I would try to come up with a way to time things now (the DP_xxx methods would do nothing, for example), and then test.

In my particular case though, I have a DAL which uses reflection pretty heavily.  I discovered a bug in my dal that had to do with threading and using objects which talk to different databases (two different Sql Server 2005 databases).  So I fixed the bug.  In my DAL I have an EntityReflector class, which does reflection on DAL objects and caches the results (PropertyInfo and MethodInfos). 

The fix in question required me to store the MethodInfo used everytime an entity is created.  So I cached it in a static field, as I've done with everything else, and ran my tests.  They passed but were noticeably slower.  So I searched out a solution.  Instead of caching the MethodInfo object, I cached the DynamicMethodDelegate object.  Ran the tests again, and things ran quickly again.  So in this case, it worked rather well. 

ajj3085 replied on Wednesday, January 09, 2008

Yes, that's point of the optimization.  You compile the DynamicDelegate (or something like that) and store it for later use.  Doing the compile each time would reduce the benefit.  Read the article again though; calling the method via its DynamicDelegate is significantly faster than using MethodInfo.Execute. 

Originally I was simply caching the MethodInfo object once I found it as well.  When I saw how slow it was, I went looking for a solution.  The solution was the DynamicDelegate.

RockfordLhotka replied on Wednesday, January 09, 2008

I’d like to see a test case that causes failure before solving a theoretical problem.

 

In other words, if there’s some combination of declared methods that can make the failure occur I’ll put it into my MethodCallerTest app as a test and that way we can establish that the solution directly solves the issue.

 

Rocky

 

 

From: vdhant [mailto:cslanet@lhotka.net]
Sent: Tuesday, January 08, 2008 5:27 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] MethodCaller: Possible Big Performance Boost??

 

Hi Rocky,
That was me who said that i think i have found a potential bug.

In the public static MethodInfo FindMethod(Type objType, string method, int parameterCount) method, it does a loop looking for methods that have a set number of paramerters. Within the loop it uses this MethodInfo info = currentType.GetMethod(method, oneLevelFlags); line which calls the GetMethod method. As far as i can see anywhere the GetMethod is used it has the ability to throw an AmbiguousMatchException exectipion. Now when you are doing calling this public static MethodInfo FindMethod(Type objType, string method, Type[] types) method, you should never get this because you can't compile with two methods of the same signature, but with the count version ther e is the possibility that it could. Hence why i thought that this is a bug and should have a try catch around it. You will see in code that i provided that it checks for this case and it the exception occurs runs the pretty much the same code that you have in the only AmbiguousMatchException expectation check at the moment. It probably has not arisen as a bug in many cases thus far because i imagine that the first findmethod would pick up most cases.

As far as a performance testing goes, I don’t really have the setup to be able to give you figures on that one, but what i do know is that there is a lot of looping that occurs to find the correct method and this looping logic, given a type, method name and a set of params is always going to produce the same result. Hence is there need to conduct that looping and reflection every time. Hence why i was trying to cache the results. I guess you now have a dictionary lookup and a lock to deal with if the method isn't found in that type but for the vast majority of cases i would imagine that there would have to be some tangible performance improvement.

Anthony



RockfordLhotka replied on Sunday, January 20, 2008

Ricky Supit and I have been working on this for the past 10 days or so. Ricky did a lot of research work and put together some very nice dynamic method code that was able to do everything MethodCaller requires. I subsequently merged that into CSLA itself.

 

The result looks pretty decent, though we’re still tweaking a bit. You can see what it looks like in svn in the C# trunk – check out Csla.Reflection (the new home for all this stuff).

 

This became a high priority once I started doing some serious perf testing on 3.5. The child object support in the data portal (which is a really big deal in terms of reducing code and making it consistent) would have been a perf killer without these dynamic method enhancements. With these enhancements there’s still a perf hit, but my guess is that most people will accept the hit to get the code savings and consistency. For those that really need optimal performance, the manual child techniques from CSLA .NET 1.x/2.x/3.0 still work of course.

 

Rocky

 

 

From: ajj3085 [mailto:cslanet@lhotka.net]
Sent: Tuesday, January 08, 2008 7:30 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] MethodCaller: Possible Big Performance Boost??

 

There's another possiblity for speeding up reflection base method calls.  I actually implemented it in my DAL when I had to add some more MethodInfo calls.  The technique can be found here.  What you do is basically do the reflection once, then compile the delegate and store it in a static field. 

I haven't done any testing, but after I added the feature that required the MethodInfo call, my unit tests ran noticibly slower.  I'm talking from a few seconds to about a minute and a half to run all the tests.  Implementing the method in the link above brought performance back to the few second range. 

If you like, I can try this in Csla.  Just tell me how you would like things timed.  I can't promise I'll get to it soon though.  Smile <img src=">

Andy


ajj3085 replied on Monday, January 21, 2008

Ahh, very cool.  I was glad when I found this too, as my DAL would have become unusable without this trick.

vdhant replied on Monday, January 21, 2008

Well i'm glad to see that something came out of this. I agree that storing the methodinfo does seem to be a bit slower for some reason but i guess there are many ways to skin a cat and it seems like a nice solution has been reached.
Thanks

Copyright (c) Marimer LLC