Yan Cui
I help clients go faster for less using serverless technologies.
This article is brought to you by
Don’t reinvent the patterns. Catalyst gives you consistent APIs for messaging, data, and workflow with key microservice patterns like circuit-breakers and retries for free.
Back in my first post on AOP, I mentioned the Memoizer on D. Patrick Caldwell’s blog, well, today I came across a situation where I was able to use it but first I needed to make a few modifications because the original implementation didn’t satisfy some of my requirements:
- There is no cap on the size of the dictionary, I want to avoid a situations where my application uses too much memory, and in the extreme case throws a OutOfMemoryException;
- There is one static cache shared across all methods, so even with a cap on the size of the dictionary it won’t stop one method from taking up all the available spaces in the dictionary
- The order of the memos is not kept, so you won’t be able to implement have FIFO strategy for removing old memo entries
In case you’re wondering why I would require these functionalities, I’m building an image viewer with zip support and it doesn’t make sense to load all the images in the zip file into memory at the start (the zip files can be typically hundreds of megs big).
The images are sorted, and as you’re navigating through the images it behaves like a LinkedList anyway, so I’m implementing a sliding window based on where you are in the list and load the previous and next 10 images into cache. Therefore a FIFO memoizer can help reduce the amount of reads from the zip file I need to perform (decompression is an expensive operation).
And here’s the modified version of the Memoizer attribute:
[Serializable] [AttributeUsage(AttributeTargets.Method)] // only allowed on methods [DebuggerStepThrough] public sealed class MemoizeAttribute : OnMethodInvocationAspect { private const int DefaultMemoSize = 100; // default memo size is 100 // private field to store memos private readonly Dictionary<string, object> _memos = new Dictionary<string, object>(); // private queue to keep track of the order the memos are put in private readonly Queue<string> _queue = new Queue<string>(); #region Constructors public MemoizeAttribute() : this(DefaultMemoSize) { } public MemoizeAttribute(int memoSize) { MemoSize = memoSize; } #endregion public int MemoSize { get; set; } // how many items to keep in the memo // intercept the method invocation public override void OnInvocation(MethodInvocationEventArgs eventArgs) { // get the arguments that were passed to the method var args = eventArgs.GetArgumentArray(); var keyBuilder = new StringBuilder(); // append the hashcode of each arg to the key // this limits us to value types (and strings) // i need a better way to do this (and preferably // a faster one) for (var i = 0; i < args.Length; i++) keyBuilder.Append(args[i].GetHashCode()); var key = keyBuilder.ToString(); // if the key doesn't exist, invoke the original method // passing the original arguments and store the result if (!_memos.ContainsKey(key)) { _memos[key] = eventArgs.Method.Invoke(eventArgs.Instance, args); _queue.Enqueue(key); // if we've exceeded the set memo size, then remove the earliest entry if (_queue.Count > MemoSize) { var deQueueKey = _queue.Dequeue(); _memos.Remove(deQueueKey); } } // return the memo eventArgs.ReturnValue = _memos[key]; } }
And here’s how you use it, remember, the size cap applies to all the calls to this method (not limited to a particular instance):
[Memoize(5)] private MemoryStream GetImageStream(ZipFile zipFile, string fileName) { using (var memoryStream = new MemoryStream()) { zipFile[fileName].Extract(memoryStream); return memoryStream; } }
Whenever you’re ready, here are 3 ways I can help you:
- Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
- I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
- Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.
Pingback: AOP with F# – Memorization using higher-order functions | theburningmonk.com