/Utilities/Cache.cs
C# | 931 lines | 528 code | 77 blank | 326 comment | 23 complexity | 12e45d8e217f11f82f4d5563a7b18ad0 MD5 | raw file
Possible License(s): Apache-2.0
- using System.Collections;
- using System.Collections.Generic;
- using System.Diagnostics;
- using Delta.Utilities.Helpers;
- using NUnit.Framework;
-
- namespace Delta.Utilities
- {
- /// <summary>
- /// Generic cache class to help getting cached data values for different
- /// input or output type. For best performance the input type should be
- /// a small value type that can be compared quickly (int, float, string)
- /// and the output type should be a class or small struct (big structs also
- /// work, but performance wise lots of copying is needed). Finally this
- /// class automatically flushes the cache after a certain period of time.
- /// <para/>
- /// Please not that this class has changed from static method to instance
- /// methods and it does not contain the delegate or names anymore. The
- /// caller now has to create its own instance and manage the delegate or
- /// code himself. This removes some cool features from the old Cache class,
- /// but it is much faster this way and allows more control and fine-tuning
- /// by each implementor.
- /// </summary>
- /// <typeparam name="TInput">Input key, should be as simple as
- /// possible and be a value type for best performance (no complex struct)
- /// </typeparam>
- /// <typeparam name="TOutput">Return value when requesting data for
- /// the input type, can be more complex or even a class.</typeparam>
- public class Cache<TInput, TOutput> : IEnumerable
- {
- #region Constants
- /// <summary>
- /// Warn when we hit 1000 cached entries and kill the whole cache. This
- /// usually does not happen in normal operation (except for heavy database
- /// use, e.g. for a website, then this needs to be increased a lot).
- /// </summary>
- internal const int WarningTooMuchCacheEntries = 1000;
-
- /// <summary>
- /// Default number of seconds till we force the next complete cache flush
- /// for each cache list! Since this is for game code we are happy with
- /// caching for up to 10 minutes, longer times are possible with the
- /// overloads below, but most simple code does not have to keep big
- /// cache lists (e.g. content caching, render list caching, etc.).
- /// Long times here have the advantage of keeping lots of cache data
- /// around and making calls faster, shorter times have the advantage of
- /// not wasting so much memory with cache data and updating the results
- /// after the flush time has run out.
- /// </summary>
- public const int DefaultFlushTimeInSeconds = 10 * 60;
- #endregion
-
- #region Delegates
- /// <summary>
- /// The method to generate the output value from the input value.
- /// </summary>
- /// <param name="input">Input type for this custom delegate</param>
- public delegate TOutput CustomCodeDelegate(TInput input);
- #endregion
-
- #region Count (Public)
- /// <summary>
- /// How many data cache entries do we currently have? This allows the
- /// caller to clear the cache if it gets too big (e.g. different font
- /// texts).
- /// </summary>
- public int Count
- {
- get
- {
- return data.Count;
- }
- }
- #endregion
-
- #region Private
-
- #region customCode (Private)
- /// <summary>
- /// Delegate for generating the output value from the input value in the
- /// Get method below.
- /// </summary>
- private readonly CustomCodeDelegate customCode;
- #endregion
-
- #region data (Private)
- /// <summary>
- /// Data we keep around in the cache
- /// </summary>
- private readonly Dictionary<TInput, TOutput> data =
- new Dictionary<TInput, TOutput>();
- #endregion
-
- #region secondsTillNextFlush (Private)
- /// <summary>
- /// Time in seconds till we do the next flush (clearing all cached data)
- /// </summary>
- private readonly int secondsTillNextFlush;
- #endregion
-
- #region nextTimeFlushedInTicks (Private)
- /// <summary>
- /// The next time all cache data is flushed down the toilet is remembered
- /// here and calculated in the constructor and once a flush happened.
- /// </summary>
- private long nextTimeFlushedInTicks;
- #endregion
-
- #region onlyCheckFlushEvery10Times (Private)
- /// <summary>
- /// Helper to make sure we only check for the flush time every 10 calls
- /// to Get. Flushing is not so important to waste too much performance on
- /// the constant calls (Stopwatch.GetTimestamp is pretty fast, but still).
- /// Without this about 60-70% of performance is lost to
- /// topwatch.GetTimestamp for simple gets, with this it is only ~20%.
- /// </summary>
- private int onlyCheckFlushEvery10Times;
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Cache constructor, which will just set the time for the next flush.
- /// </summary>
- /// <param name="setCustomCode">Custom code to execute when a input
- /// key is not found and the output value needs to be generated.</param>
- /// <param name="setSecondsTillNextFlush">Set time till next flush, by
- /// default this value is 10 minutes (10*60).</param>
- public Cache(CustomCodeDelegate setCustomCode, int setSecondsTillNextFlush)
- {
- customCode = setCustomCode;
- secondsTillNextFlush = setSecondsTillNextFlush;
- nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
- Stopwatch.Frequency * secondsTillNextFlush;
-
- // Link up our Clear method to kill all cached data when memory is low!
- ArrayHelper.FreeDataWhenMemoryIsLow += Clear;
- }
-
- /// <summary>
- /// Cache constructor with the default cache flush time of 10 minutes.
- /// </summary>
- /// <param name="setCustomCode">Custom code to execute when a input
- /// key is not found and the output value needs to be generated.</param>
- public Cache(CustomCodeDelegate setCustomCode)
- : this(setCustomCode, DefaultFlushTimeInSeconds)
- {
- }
- #endregion
-
- #region IEnumerable Members
- /// <summary>
- /// Returns an enumerator that iterates through the cache values.
- /// </summary>
- /// <returns>
- /// An <see cref="T:System.Collections.IEnumerator"/> object that can be
- /// used to iterate through the collection.
- /// </returns>
- public IEnumerator GetEnumerator()
- {
- return data.GetEnumerator();
- }
- #endregion
-
- #region Add (Public)
- /// <summary>
- /// Add a new input value with a specific output value, will update
- /// an existing item if it already exists.
- /// </summary>
- /// <param name="input">Input</param>
- /// <param name="value">Value</param>
- public void Add(TInput input, TOutput value)
- {
- // Got no value yet for that input?
- if (data.ContainsKey(input) == false)
- {
- // Then just create it.
- data.Add(input, value);
-
- // Note: Hopefully this is not slow, else only do this in DEBUG mode
- if (data.Count > WarningTooMuchCacheEntries)
- {
- Log.Warning("Reached " + data.Count + " added cache entries. " +
- "This should not happen and the cache will be completely " +
- "cleared now to prevent performance impacts! " +
- "secondsTillNextFlush=" + secondsTillNextFlush);
- Clear();
- } // if (addedCacheEntries)
- } // if (data.ContainsKey)
- else
- {
- // A simple update is fine this way. Note: Updates should
- // not happen that often, the cache should be cleared instead.
- data[input] = value;
- }
- }
- #endregion
-
- #region Remove (Public)
- /// <summary>
- /// Remove cached value in case we updated something and want to clear a
- /// specific cache entry.
- /// </summary>
- /// <param name="input">Input</param>
- public void Remove(TInput input)
- {
- data.Remove(input);
- }
- #endregion
-
- #region Clear (Public)
- /// <summary>
- /// Clear whole cache, updated lastTimeFlushed time and clear statistics
- /// </summary>
- public void Clear()
- {
- data.Clear();
- nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
- Stopwatch.Frequency * secondsTillNextFlush;
-
- //Note: We could also call GC.Collect to free some memory.
- //GC.Collect();
- }
- #endregion
-
- #region Exists
- /// <summary>
- /// Check if an entry exists for the given input value. This will not get
- /// the actual cached output value or create a new cached entry like the
- /// Get method does.
- /// </summary>
- /// <param name="input">Input</param>
- /// <returns>True if the cache entry was found, false otherwise</returns>
- public bool Exists(TInput input)
- {
- return data.ContainsKey(input);
- }
- #endregion
-
- #region Get (Public)
- /// <summary>
- /// Get method, which gets the cached data, most important method here.
- /// </summary>
- /// <param name="input">Input</param>
- public TOutput Get(TInput input)
- {
- // Do we have to clear the whole cache because the data is too old?
- if ((onlyCheckFlushEvery10Times++) % 10 == 0 &&
- Stopwatch.GetTimestamp() > nextTimeFlushedInTicks)
- {
- Clear();
- }
-
- // Check if already have that data cached!
- TOutput ret;
- if (data.TryGetValue(input, out ret))
- {
- return ret;
- }
-
- // Not cached? Then get data the slow way with the custom code delegate.
- ret = customCode(input);
- Add(input, ret);
- return ret;
- }
- #endregion
-
- #region GetAll (Public)
- /// <summary>
- /// Helper method to get all entries used in this cache.
- /// </summary>
- /// <returns>Copied list of the cached output types</returns>
- public List<TOutput> GetAll()
- {
- return new List<TOutput>(data.Values);
- }
- #endregion
- }
-
- /// <summary>
- /// Same Cache class as above, but allows to have multiple input types,
- /// which is not an easy problem if you are targeting high performance
- /// dictionary gets. Here is a good article about this topic:
- /// http://www.aronweiler.com/2011/05/performance-tests-between-multiple.html
- /// <para />
- /// For now the easiest way to do this is used by having 2 dictionaries
- /// inside each other. The first key is the important one.
- /// </summary>
- /// <typeparam name="TInput1">First and important input key, should be as
- /// simple as possible and be a value type for best performance</typeparam>
- /// <typeparam name="TInput2">Second input key, only if both the first
- /// and second input key match, the output value is returned.</typeparam>
- /// <typeparam name="TOutput">Return value when requesting data for
- /// the input type, can be more complex or even a class.</typeparam>
- public class Cache<TInput1, TInput2, TOutput>
- {
- #region Constants
- /// <summary>
- /// Warn when we hit 1000 cached entries and kill the whole cache. This
- /// usually does not happen in normal operation (except for heavy database
- /// use, e.g. for a website, then this needs to be increased a lot).
- /// </summary>
- internal const int WarningTooMuchCacheEntries = 1000;
-
- /// <summary>
- /// Default number of seconds till we force the next complete cache flush
- /// for each cache list! Since this is for game code we are happy with
- /// caching for up to 10 minutes, longer times are possible with the
- /// overloads below, but most simple code does not have to keep big
- /// cache lists (e.g. content caching, render list caching, etc.).
- /// Long times here have the advantage of keeping lots of cache data
- /// around and making calls faster, shorter times have the advantage of
- /// not wasting so much memory with cache data and updating the results
- /// after the flush time has run out.
- /// </summary>
- public const int DefaultFlushTimeInSeconds = 10 * 60;
- #endregion
-
- #region Delegates
- /// <summary>
- /// The method to generate the output value from the input values.
- /// </summary>
- /// <param name="input1">Input type 1 for this custom delegate</param>
- /// <param name="input2">Input type 2 for this custom delegate</param>
- public delegate TOutput CustomCodeDelegate(TInput1 input1,
- TInput2 input2);
- #endregion
-
- #region Count (Public)
- /// <summary>
- /// How many data cache entries do we currently have? This allows the
- /// caller to clear the cache if it gets too big (e.g. different font
- /// texts).
- /// </summary>
- public int Count
- {
- get
- {
- return data.Count;
- }
- }
- #endregion
-
- #region Private
-
- #region customCode (Private)
- /// <summary>
- /// Delegate for generating the output value from the input value in the
- /// Get method below.
- /// </summary>
- private readonly CustomCodeDelegate customCode;
- #endregion
-
- #region data (Private)
- /// <summary>
- /// Data we keep around in the cache
- /// </summary>
- private readonly Dictionary<TInput1, Dictionary<TInput2, TOutput>> data =
- new Dictionary<TInput1, Dictionary<TInput2, TOutput>>();
- #endregion
-
- #region secondsTillNextFlush (Private)
- /// <summary>
- /// Time in seconds till we do the next flush (clearing all cached data)
- /// </summary>
- private readonly int secondsTillNextFlush;
- #endregion
-
- #region nextTimeFlushedInTicks (Private)
- /// <summary>
- /// The next time all cache data is flushed down the toilet is remembered
- /// here and calculated in the constructor and once a flush happened.
- /// </summary>
- private long nextTimeFlushedInTicks;
- #endregion
-
- #region onlyCheckFlushEvery10Times (Private)
- /// <summary>
- /// Helper to make sure we only check for the flush time every 10 calls
- /// to Get. Flushing is not so important to waste too much performance on
- /// the constant calls (Stopwatch.GetTimestamp is pretty fast, but still).
- /// Without this about 60-70% of performance is lost to
- /// topwatch.GetTimestamp for simple gets, with this it is only ~20%.
- /// </summary>
- private int onlyCheckFlushEvery10Times;
- #endregion
-
- #endregion
-
- #region Constructors
- /// <summary>
- /// Cache constructor, which will just set the time for the next flush.
- /// </summary>
- /// <param name="setSecondsTillNextFlush">Set time till next flush</param>
- /// <param name="setCustomCode">Custom code to execute when a input
- /// key is not found and the output value needs to be generated.</param>
- public Cache(CustomCodeDelegate setCustomCode, int setSecondsTillNextFlush)
- {
- customCode = setCustomCode;
- secondsTillNextFlush = setSecondsTillNextFlush;
- nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
- Stopwatch.Frequency * secondsTillNextFlush;
- }
-
- /// <summary>
- /// Cache constructor with the default cache flush time of 10 minutes.
- /// </summary>
- /// <param name="setCustomCode">Custom code to execute when a input
- /// key is not found and the output value needs to be generated.</param>
- public Cache(CustomCodeDelegate setCustomCode)
- : this(setCustomCode, DefaultFlushTimeInSeconds)
- {
- }
- #endregion
-
- #region Add (Public)
- /// <summary>
- /// Add a new input value with a specific output value, will update
- /// an existing item if it already exists.
- /// </summary>
- /// <param name="input1">Input key part 1, must match</param>
- /// <param name="input2">Input key part 2, must also match</param>
- /// <param name="value">Value</param>
- public void Add(TInput1 input1, TInput2 input2, TOutput value)
- {
- // Got no value yet for that input1?
- if (data.ContainsKey(input1) == false)
- {
- // Then we need to create it in any case
- Dictionary<TInput2, TOutput> innerData =
- new Dictionary<TInput2, TOutput>();
- innerData.Add(input2, value);
- data.Add(input1, innerData);
-
- // Note: Hopefully this is not slow, else only do this in DEBUG mode
- if (data.Count > WarningTooMuchCacheEntries)
- {
- Log.Warning("Reached " + data.Count + " added cache entries. " +
- "This should not happen and the cache will be completely " +
- "cleared now to prevent performance impacts! " +
- "secondsTillNextFlush=" + secondsTillNextFlush);
- Clear();
- } // if (addedCacheEntries)
- } // if (data.ContainsKey)
- else
- {
- // First check if we also have the second input value
- Dictionary<TInput2, TOutput> innerData = data[input1];
- if (innerData.ContainsKey(input2) == false)
- {
- // Not found, then the inner data needs to be created.
- innerData.Add(input2, value);
- }
- else
- {
- // Otherwise a simple update is fine. Note: Updates should
- // not happen that often, the cache should be cleared instead.
- innerData[input2] = value;
- }
- }
- }
- #endregion
-
- #region Remove (Public)
- /// <summary>
- /// Remove cached value in case we updated something and want to clear a
- /// specific cache entry. Note: Will not kill any dictionaries as data
- /// will most likely be filled in soon. Use Clear to kill everything!
- /// </summary>
- /// <param name="input1">Input key part 1, must match</param>
- /// <param name="input2">Input key part 2, must also match</param>
- public void Remove(TInput1 input1, TInput2 input2)
- {
- if (data.ContainsKey(input1))
- {
- data[input1].Remove(input2);
- }
- }
- #endregion
-
- #region Clear (Public)
- /// <summary>
- /// Clear whole cache, updated lastTimeFlushed time and clear statistics
- /// </summary>
- public void Clear()
- {
- data.Clear();
- nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
- Stopwatch.Frequency * secondsTillNextFlush;
-
- //Note: We could also call GC.Collect to free some memory.
- //GC.Collect();
- }
- #endregion
-
- #region Exists
- /// <summary>
- /// Check if an entry exists for the given input value. This will not get
- /// the actual cached output value or create a new cached entry like the
- /// Get method does.
- /// </summary>
- /// <param name="input1">First input key part</param>
- /// <param name="input2">Second input key part</param>
- /// <returns>True if the cache entry was found, false otherwise</returns>
- public bool Exists(TInput1 input1, TInput2 input2)
- {
- // First check the outer list
- Dictionary<TInput2, TOutput> innerData;
- if (data.TryGetValue(input1, out innerData))
- {
- // And then do a simple ContainsKey check in the inner dictionary
- return innerData.ContainsKey(input2);
- }
-
- // Otherwise the entry was not even found in the outer dictionary
- return false;
- }
- #endregion
-
- #region Get (Public)
- /// <summary>
- /// Get method, which gets the cached data, most important method here.
- /// </summary>
- /// <param name="input1">First input key part</param>
- /// <param name="input2">Second input key part</param>
- /// <returns>Output Type</returns>
- public TOutput Get(TInput1 input1, TInput2 input2)
- {
- // Do we have to clear the whole cache because the data is too old?
- if ((onlyCheckFlushEvery10Times++) % 10 == 0 &&
- Stopwatch.GetTimestamp() > nextTimeFlushedInTicks)
- {
- Clear();
- }
-
- // Check if already have that data cached!
- TOutput ret;
- Dictionary<TInput2, TOutput> innerData;
- if (data.TryGetValue(input1, out innerData))
- {
- if (innerData.TryGetValue(input2, out ret))
- {
- return ret;
- }
- }
-
- // Not cached? Then get data the slow way with the custom code delegate.
- ret = customCode(input1, input2);
- Add(input1, input2, ret);
- return ret;
- }
- #endregion
-
- #region GetAllEntries (Public)
- /// <summary>
- /// Return all entries as a flat list so we can easily enumerate though
- /// them. Useful for disposing stuff.
- /// </summary>
- /// <returns>Flat list of TOutput</returns>
- public IEnumerable<TOutput> GetAllEntries()
- {
- List<TOutput> ret = new List<TOutput>();
- foreach (Dictionary<TInput2, TOutput> innerData in data.Values)
- {
- foreach (TOutput value in innerData.Values)
- {
- ret.Add(value);
- }
- }
- return ret;
- }
- #endregion
- }
-
- #region CacheTests
- /// <summary>
- /// Tests for the generic Cache class (cannot do tests directly there).
- /// </summary>
- internal class CacheTests
- {
- #region Helpers
- /// <summary>
- /// Simple struct for the complexStructCache in TestPerformance.
- /// </summary>
- private struct SimpleStruct
- {
- #region X (Public)
- public float X;
- #endregion
-
- #region Y (Public)
- public float Y;
- #endregion
-
- #region Number (Public)
- public int Number;
- #endregion
- }
- #endregion
-
- #region Helpers
- /// <summary>
- /// Helper struct for the input type of the ComplexInputAndOutputData test
- /// </summary>
- private struct StringAndNumber
- {
- #region Text (Public)
- public string Text;
- #endregion
-
- #region Number (Public)
- public int Number;
- #endregion
- }
- #endregion
-
- #region Helpers
- /// <summary>
- /// Helper struct for the output type of the ComplexInputAndOutputData test
- /// </summary>
- private struct LotsOfNumbers
- {
- #region Number1 (Public)
- public int Number1;
- #endregion
-
- #region Number2 (Public)
- public int Number2;
- #endregion
-
- #region Number3 (Public)
- public int Number3;
- #endregion
- }
- #endregion
-
- #region TestPerformance (LongRunning)
- /// <summary>
- /// Performance test of the Cache class, doing 1 million gets.
- /// <para />
- /// Result is for 1 million iterations each (with a 4Ghz Sandy Bridge CPU):
- /// <para />
- /// simpleIntCache (int in, int out): ~20ms
- /// <para />
- /// stringLengthCache (string in, int out): ~35ms
- /// <para />
- /// complexCache (string in, float in, string out): ~55ms
- /// <para />
- /// complexStructCache (string in, Struct in, string out): ~140ms
- /// <para />
- /// structCache (struct with string+int in, struct out): ~1200ms
- /// <para />
- /// Note: The last one (structCache) is much slower than the rest and the
- /// reason for this is the struct (string+int). Mixing string and other
- /// datatypes is slow as hell when comparing structs, even the
- /// complexStructCache version is slower than the rest with just 2 floats
- /// and an integer number. All this need to be copied to the Get method
- /// and then compared to all other dictionary values, which takes time!
- /// Always use the generic Cache class with 2 inputs if you have a string
- /// plus other data, and try to simplify the input key as much as possible.
- /// </summary>
- [Test]
- [Category("LongRunning")]
- public static void TestPerformance()
- {
- // Iterate for 1 million times.
- const int Iterations = 1000000;
-
- // Simple Cache<int, int> test (number in, number out, fastest)
- Cache<int, int> simpleIntCache = new Cache<int, int>(
- delegate(int input)
- {
- return input * 2;
- });
- Assert.Equal(4, simpleIntCache.Get(2));
-
- // Now do it many times
- long startTime = Stopwatch.GetTimestamp();
- for (int num = 0; num < Iterations; num++)
- {
- simpleIntCache.Get(2);
- }
- long endTime = Stopwatch.GetTimestamp();
- Log.Info(Iterations + " iterations of Cache<int, int>.Get took " +
- ((endTime - startTime) * 1000 / Stopwatch.Frequency) + "ms");
-
- // Next test a Cache<string, int> version
- Cache<string, int> stringLengthCache = new Cache<string, int>(
- delegate(string input)
- {
- return input.Length;
- });
- Assert.Equal(9, stringLengthCache.Get("What's up"));
-
- startTime = Stopwatch.GetTimestamp();
- for (int num = 0; num < Iterations; num++)
- {
- stringLengthCache.Get("What's up");
- }
- endTime = Stopwatch.GetTimestamp();
- Log.Info(Iterations + " iterations of Cache<string, int>.Get took " +
- ((endTime - startTime) * 1000 / Stopwatch.Frequency) + "ms");
-
- // Test a more complex example with Cache<string, float, string>
- Cache<string, float, string> complexCache =
- new Cache<string, float, string>(
- delegate(string inputText, float inputValue)
- {
- // Just build a pretty simple string from the input values
- return inputText + " " + inputValue.ToInvariantString();
- });
- Assert.Equal("Hi there 5.2", complexCache.Get("Hi there", 5.2f));
-
- startTime = Stopwatch.GetTimestamp();
- for (int num = 0; num < Iterations; num++)
- {
- complexCache.Get("Hi there", 5.2f);
- }
- endTime = Stopwatch.GetTimestamp();
- Log.Info(Iterations + " iterations of Cache<string, float, string>." +
- "Get took " + ((endTime - startTime) * 1000 / Stopwatch.Frequency) +
- "ms");
-
- // Also test a complex example with struct
- Cache<string, SimpleStruct, string> complexStructCache =
- new Cache<string, SimpleStruct, string>(
- delegate(string inputText, SimpleStruct inputStruct)
- {
- // Just build a pretty simple string from the input values
- return inputText + " " +
- inputStruct.X.ToInvariantString() + ", " +
- inputStruct.Y.ToInvariantString() + ", " +
- inputStruct.Number;
- });
- Assert.Equal("Numbers: 1.2, 2.3, 9", complexStructCache.Get("Numbers:",
- new SimpleStruct
- {
- X = 1.2f,
- Y = 2.3f,
- Number = 9,
- }));
-
- startTime = Stopwatch.GetTimestamp();
- for (int num = 0; num < Iterations; num++)
- {
- complexStructCache.Get("Numbers:",
- new SimpleStruct
- {
- X = 1.2f,
- Y = 2.3f,
- Number = 9,
- });
- }
- endTime = Stopwatch.GetTimestamp();
- Log.Info(Iterations + " iterations of Cache<string, SimpleStruct, " +
- "string>.Get took " + ((endTime - startTime) * 1000 /
- Stopwatch.Frequency) + "ms");
-
- // And finally test the slowest way with a complex struct, which should
- // be avoided at all cost (the complex case using the 2 input key Cache
- // above is much faster with almost the same kind of data).
- Cache<StringAndNumber, LotsOfNumbers> structCache =
- new Cache<StringAndNumber, LotsOfNumbers>(
- delegate(StringAndNumber input)
- {
- return new LotsOfNumbers
- {
- Number1 = input.Number,
- Number2 = input.Text.Length,
- Number3 = input.Number + input.Text.Length,
- };
- });
- Assert.Equal(5, structCache.Get(
- new StringAndNumber
- {
- Text = "Hi",
- Number = 5,
- }).Number1);
-
- // Now do it many times
- startTime = Stopwatch.GetTimestamp();
- for (int num = 0; num < Iterations; num++)
- {
- // Always use the same input data for quick performance test
- structCache.Get(
- // Still a new struct has to be constructed each time
- new StringAndNumber
- {
- Text = "Hi",
- Number = 5,
- });
- }
- endTime = Stopwatch.GetTimestamp();
- Log.Info(Iterations + " iterations of Cache<StringAndNumber, " +
- "LotsOfNumbers>.Get took " + ((endTime - startTime) * 1000 /
- Stopwatch.Frequency) +
- "ms (avoid this obviously)");
- }
- #endregion
-
- #region GetCachedData
- /// <summary>
- /// Test get cached data
- /// </summary>
- [Test]
- public void GetCachedData()
- {
- // Test very simple static cache with just one return value no matter
- // what the input is.
- Cache<int, string> hiCache = new Cache<int, string>(
- delegate
- {
- return "Hi";
- });
- Assert.Equal("Hi", hiCache.Get(1));
-
- // Next test a more complex example with many return values
- Cache<string, int> getNumberCache = new Cache<string, int>(
- delegate(string input)
- {
- switch (input)
- {
- case "One":
- return 1;
- case "Two":
- return 2;
- case "Ten":
- return 10;
- default:
- return 0;
- } // switch
- });
- Assert.Equal(1, getNumberCache.Get("One"));
- // Try some more gets
- Assert.Equal(2, getNumberCache.Get("Two"));
- Assert.Equal(0, getNumberCache.Get("Something else"));
- Assert.Equal(10, getNumberCache.Get("Ten"));
-
- // Next up is a little delegate that just returns the length of a string
- int numberOfTimeStringLengthCalled = 0;
- Cache<string, int> stringLengthCache = new Cache<string, int>(
- delegate(string input)
- {
- numberOfTimeStringLengthCalled++;
- return input.Length;
- });
- Assert.Equal(3, stringLengthCache.Get("abc"));
- Assert.Equal(2, stringLengthCache.Get("de"));
- Assert.Equal(5, stringLengthCache.Get("benny"));
- // Try getting a few more times
- Assert.Equal(2, stringLengthCache.Get("de"));
- Assert.Equal(5, stringLengthCache.Get("benny"));
- // Overall the cache delegate should only have been called 3 times
- Assert.Equal(3, numberOfTimeStringLengthCalled);
- }
- #endregion
-
- // GetCachedData()
-
- #region ClearCachedData
- /// <summary>
- /// Clear cached data
- /// </summary>
- [Test]
- public void ClearCachedData()
- {
- int cacheGetCounter = 0;
- Cache<int, int> doubleNumberCache = new Cache<int, int>(
- delegate(int inputNumber)
- {
- cacheGetCounter++;
- return inputNumber * 2;
- });
-
- // Do some gets, make sure each get is only executed once
- Assert.Equal(0, cacheGetCounter);
- Assert.Equal(4, doubleNumberCache.Get(2));
- Assert.Equal(1, cacheGetCounter);
- Assert.Equal(4, doubleNumberCache.Get(2));
- Assert.Equal(1, cacheGetCounter);
- // And call it with a different number
- Assert.Equal(6, doubleNumberCache.Get(3));
- Assert.Equal(2, cacheGetCounter);
-
- // Delete a cached value and see if getting it again will execute
- // doubleNumber once again.
- doubleNumberCache.Clear();
- Assert.Equal(2, cacheGetCounter);
- Assert.Equal(4, doubleNumberCache.Get(2));
- // Get counter should be increased now again
- Assert.Equal(3, cacheGetCounter);
- // And test some more gets, which should access cached data now again
- Assert.Equal(4, doubleNumberCache.Get(2));
- Assert.Equal(4, doubleNumberCache.Get(2));
- Assert.Equal(3, cacheGetCounter);
- }
- #endregion
-
- #region MultipleInputKeys
- /// <summary>
- /// Unit test to show of a more complex example with multiple input keys.
- /// </summary>
- [Test]
- public void MultipleInputKeys()
- {
- Cache<string, int, LotsOfNumbers> multipleKeysCache =
- new Cache<string, int, LotsOfNumbers>(
- delegate(string inputText, int inputNumber)
- {
- return new LotsOfNumbers
- {
- Number1 = inputNumber,
- //Note: Intentionally wrong code to test if more delegates are
- // generated then there should be (5 instead of 1)!
- //Number2 = text.Length,
- // And this is the correct code!
- Number2 = inputText.Length,
- Number3 = inputNumber + inputText.Length,
- };
- });
-
- Assert.Equal(1, multipleKeysCache.Get("", 1).Number1);
- Assert.Equal(0, multipleKeysCache.Get("", 1).Number2);
- Assert.Equal(5, multipleKeysCache.Get("Hi", 3).Number3);
- Assert.Equal(9, multipleKeysCache.Get("Whats up", 1).Number3);
- }
- #endregion
- }
- #endregion
- }