PageRenderTime 482ms CodeModel.GetById 160ms app.highlight 184ms RepoModel.GetById 89ms app.codeStats 0ms

/Utilities/Cache.cs

#
C# | 931 lines | 528 code | 77 blank | 326 comment | 23 complexity | 12e45d8e217f11f82f4d5563a7b18ad0 MD5 | raw file
  1using System.Collections;
  2using System.Collections.Generic;
  3using System.Diagnostics;
  4using Delta.Utilities.Helpers;
  5using NUnit.Framework;
  6
  7namespace Delta.Utilities
  8{
  9	/// <summary>
 10	/// Generic cache class to help getting cached data values for different
 11	/// input or output type. For best performance the input type should be
 12	/// a small value type that can be compared quickly (int, float, string)
 13	/// and the output type should be a class or small struct (big structs also
 14	/// work, but performance wise lots of copying is needed). Finally this
 15	/// class automatically flushes the cache after a certain period of time.
 16	/// <para/>
 17	/// Please not that this class has changed from static method to instance
 18	/// methods and it does not contain the delegate or names anymore. The
 19	/// caller now has to create its own instance and manage the delegate or
 20	/// code himself. This removes some cool features from the old Cache class,
 21	/// but it is much faster this way and allows more control and fine-tuning
 22	/// by each implementor.
 23	/// </summary>
 24	/// <typeparam name="TInput">Input key, should be as simple as
 25	/// possible and be a value type for best performance (no complex struct)
 26	/// </typeparam>
 27	/// <typeparam name="TOutput">Return value when requesting data for
 28	/// the input type, can be more complex or even a class.</typeparam>
 29	public class Cache<TInput, TOutput> : IEnumerable
 30	{
 31		#region Constants
 32		/// <summary>
 33		/// Warn when we hit 1000 cached entries and kill the whole cache. This
 34		/// usually does not happen in normal operation (except for heavy database
 35		/// use, e.g. for a website, then this needs to be increased a lot).
 36		/// </summary>
 37		internal const int WarningTooMuchCacheEntries = 1000;
 38
 39		/// <summary>
 40		/// Default number of seconds till we force the next complete cache flush
 41		/// for each cache list! Since this is for game code we are happy with
 42		/// caching for up to 10 minutes, longer times are possible with the
 43		/// overloads below, but most simple code does not have to keep big
 44		/// cache lists (e.g. content caching, render list caching, etc.).
 45		/// Long times here have the advantage of keeping lots of cache data
 46		/// around and making calls faster, shorter times have the advantage of
 47		/// not wasting so much memory with cache data and updating the results
 48		/// after the flush time has run out.
 49		/// </summary>
 50		public const int DefaultFlushTimeInSeconds = 10 * 60;
 51		#endregion
 52
 53		#region Delegates
 54		/// <summary>
 55		/// The method to generate the output value from the input value.
 56		/// </summary>
 57		/// <param name="input">Input type for this custom delegate</param>
 58		public delegate TOutput CustomCodeDelegate(TInput input);
 59		#endregion
 60
 61		#region Count (Public)
 62		/// <summary>
 63		/// How many data cache entries do we currently have? This allows the
 64		/// caller to clear the cache if it gets too big (e.g. different font
 65		/// texts).
 66		/// </summary>
 67		public int Count
 68		{
 69			get
 70			{
 71				return data.Count;
 72			}
 73		}
 74		#endregion
 75
 76		#region Private
 77
 78		#region customCode (Private)
 79		/// <summary>
 80		/// Delegate for generating the output value from the input value in the
 81		/// Get method below.
 82		/// </summary>
 83		private readonly CustomCodeDelegate customCode;
 84		#endregion
 85
 86		#region data (Private)
 87		/// <summary>
 88		/// Data we keep around in the cache
 89		/// </summary>
 90		private readonly Dictionary<TInput, TOutput> data =
 91			new Dictionary<TInput, TOutput>();
 92		#endregion
 93
 94		#region secondsTillNextFlush (Private)
 95		/// <summary>
 96		/// Time in seconds till we do the next flush (clearing all cached data)
 97		/// </summary>
 98		private readonly int secondsTillNextFlush;
 99		#endregion
100
101		#region nextTimeFlushedInTicks (Private)
102		/// <summary>
103		/// The next time all cache data is flushed down the toilet is remembered
104		/// here and calculated in the constructor and once a flush happened.
105		/// </summary>
106		private long nextTimeFlushedInTicks;
107		#endregion
108
109		#region onlyCheckFlushEvery10Times (Private)
110		/// <summary>
111		/// Helper to make sure we only check for the flush time every 10 calls
112		/// to Get. Flushing is not so important to waste too much performance on
113		/// the constant calls (Stopwatch.GetTimestamp is pretty fast, but still).
114		/// Without this about 60-70% of performance is lost to
115		/// topwatch.GetTimestamp for simple gets, with this it is only ~20%.
116		/// </summary>
117		private int onlyCheckFlushEvery10Times;
118		#endregion
119
120		#endregion
121
122		#region Constructors
123		/// <summary>
124		/// Cache constructor, which will just set the time for the next flush.
125		/// </summary>
126		/// <param name="setCustomCode">Custom code to execute when a input
127		/// key is not found and the output value needs to be generated.</param>
128		/// <param name="setSecondsTillNextFlush">Set time till next flush, by
129		/// default this value is 10 minutes (10*60).</param>
130		public Cache(CustomCodeDelegate setCustomCode, int setSecondsTillNextFlush)
131		{
132			customCode = setCustomCode;
133			secondsTillNextFlush = setSecondsTillNextFlush;
134			nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
135			                         Stopwatch.Frequency * secondsTillNextFlush;
136
137			// Link up our Clear method to kill all cached data when memory is low!
138			ArrayHelper.FreeDataWhenMemoryIsLow += Clear;
139		}
140
141		/// <summary>
142		/// Cache constructor with the default cache flush time of 10 minutes.
143		/// </summary>
144		/// <param name="setCustomCode">Custom code to execute when a input
145		/// key is not found and the output value needs to be generated.</param>
146		public Cache(CustomCodeDelegate setCustomCode)
147			: this(setCustomCode, DefaultFlushTimeInSeconds)
148		{
149		}
150		#endregion
151
152		#region IEnumerable Members
153		/// <summary>
154		/// Returns an enumerator that iterates through the cache values.
155		/// </summary>
156		/// <returns>
157		/// An <see cref="T:System.Collections.IEnumerator"/> object that can be
158		/// used to iterate through the collection.
159		/// </returns>
160		public IEnumerator GetEnumerator()
161		{
162			return data.GetEnumerator();
163		}
164		#endregion
165
166		#region Add (Public)
167		/// <summary>
168		/// Add a new input value with a specific output value, will update
169		/// an existing item if it already exists.
170		/// </summary>
171		/// <param name="input">Input</param>
172		/// <param name="value">Value</param>
173		public void Add(TInput input, TOutput value)
174		{
175			// Got no value yet for that input?
176			if (data.ContainsKey(input) == false)
177			{
178				// Then just create it.
179				data.Add(input, value);
180
181				// Note: Hopefully this is not slow, else only do this in DEBUG mode
182				if (data.Count > WarningTooMuchCacheEntries)
183				{
184					Log.Warning("Reached " + data.Count + " added cache entries. " +
185					            "This should not happen and the cache will be completely " +
186					            "cleared now to prevent performance impacts! " +
187					            "secondsTillNextFlush=" + secondsTillNextFlush);
188					Clear();
189				} // if (addedCacheEntries)
190			} // if (data.ContainsKey)
191			else
192			{
193				// A simple update is fine this way. Note: Updates should
194				// not happen that often, the cache should be cleared instead.
195				data[input] = value;
196			}
197		}
198		#endregion
199
200		#region Remove (Public)
201		/// <summary>
202		/// Remove cached value in case we updated something and want to clear a
203		/// specific cache entry.
204		/// </summary>
205		/// <param name="input">Input</param>
206		public void Remove(TInput input)
207		{
208			data.Remove(input);
209		}
210		#endregion
211
212		#region Clear (Public)
213		/// <summary>
214		/// Clear whole cache, updated lastTimeFlushed time and clear statistics
215		/// </summary>
216		public void Clear()
217		{
218			data.Clear();
219			nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
220			                         Stopwatch.Frequency * secondsTillNextFlush;
221
222			//Note: We could also call GC.Collect to free some memory.
223			//GC.Collect();
224		}
225		#endregion
226
227		#region Exists
228		/// <summary>
229		/// Check if an entry exists for the given input value. This will not get
230		/// the actual cached output value or create a new cached entry like the
231		/// Get method does.
232		/// </summary>
233		/// <param name="input">Input</param>
234		/// <returns>True if the cache entry was found, false otherwise</returns>
235		public bool Exists(TInput input)
236		{
237			return data.ContainsKey(input);
238		}
239		#endregion
240
241		#region Get (Public)
242		/// <summary>
243		/// Get method, which gets the cached data, most important method here.
244		/// </summary>
245		/// <param name="input">Input</param>
246		public TOutput Get(TInput input)
247		{
248			// Do we have to clear the whole cache because the data is too old?
249			if ((onlyCheckFlushEvery10Times++) % 10 == 0 &&
250			    Stopwatch.GetTimestamp() > nextTimeFlushedInTicks)
251			{
252				Clear();
253			}
254
255			// Check if already have that data cached!
256			TOutput ret;
257			if (data.TryGetValue(input, out ret))
258			{
259				return ret;
260			}
261			
262			// Not cached? Then get data the slow way with the custom code delegate.
263			ret = customCode(input);
264			Add(input, ret);
265			return ret;
266		}
267		#endregion
268
269		#region GetAll (Public)
270		/// <summary>
271		/// Helper method to get all entries used in this cache.
272		/// </summary>
273		/// <returns>Copied list of the cached output types</returns>
274		public List<TOutput> GetAll()
275		{
276			return new List<TOutput>(data.Values);
277		}
278		#endregion
279	}
280
281	/// <summary>
282	/// Same Cache class as above, but allows to have multiple input types,
283	/// which is not an easy problem if you are targeting high performance
284	/// dictionary gets. Here is a good article about this topic:
285	/// http://www.aronweiler.com/2011/05/performance-tests-between-multiple.html
286	/// <para />
287	/// For now the easiest way to do this is used by having 2 dictionaries
288	/// inside each other. The first key is the important one.
289	/// </summary>
290	/// <typeparam name="TInput1">First and important input key, should be as
291	/// simple as possible and be a value type for best performance</typeparam>
292	/// <typeparam name="TInput2">Second input key, only if both the first
293	/// and second input key match, the output value is returned.</typeparam>
294	/// <typeparam name="TOutput">Return value when requesting data for
295	/// the input type, can be more complex or even a class.</typeparam>
296	public class Cache<TInput1, TInput2, TOutput>
297	{
298		#region Constants
299		/// <summary>
300		/// Warn when we hit 1000 cached entries and kill the whole cache. This
301		/// usually does not happen in normal operation (except for heavy database
302		/// use, e.g. for a website, then this needs to be increased a lot).
303		/// </summary>
304		internal const int WarningTooMuchCacheEntries = 1000;
305
306		/// <summary>
307		/// Default number of seconds till we force the next complete cache flush
308		/// for each cache list! Since this is for game code we are happy with
309		/// caching for up to 10 minutes, longer times are possible with the
310		/// overloads below, but most simple code does not have to keep big
311		/// cache lists (e.g. content caching, render list caching, etc.).
312		/// Long times here have the advantage of keeping lots of cache data
313		/// around and making calls faster, shorter times have the advantage of
314		/// not wasting so much memory with cache data and updating the results
315		/// after the flush time has run out.
316		/// </summary>
317		public const int DefaultFlushTimeInSeconds = 10 * 60;
318		#endregion
319
320		#region Delegates
321		/// <summary>
322		/// The method to generate the output value from the input values.
323		/// </summary>
324		/// <param name="input1">Input type 1 for this custom delegate</param>
325		/// <param name="input2">Input type 2 for this custom delegate</param>
326		public delegate TOutput CustomCodeDelegate(TInput1 input1,
327			TInput2 input2);
328		#endregion
329
330		#region Count (Public)
331		/// <summary>
332		/// How many data cache entries do we currently have? This allows the
333		/// caller to clear the cache if it gets too big (e.g. different font
334		/// texts).
335		/// </summary>
336		public int Count
337		{
338			get
339			{
340				return data.Count;
341			}
342		}
343		#endregion
344
345		#region Private
346
347		#region customCode (Private)
348		/// <summary>
349		/// Delegate for generating the output value from the input value in the
350		/// Get method below.
351		/// </summary>
352		private readonly CustomCodeDelegate customCode;
353		#endregion
354
355		#region data (Private)
356		/// <summary>
357		/// Data we keep around in the cache
358		/// </summary>
359		private readonly Dictionary<TInput1, Dictionary<TInput2, TOutput>> data =
360			new Dictionary<TInput1, Dictionary<TInput2, TOutput>>();
361		#endregion
362
363		#region secondsTillNextFlush (Private)
364		/// <summary>
365		/// Time in seconds till we do the next flush (clearing all cached data)
366		/// </summary>
367		private readonly int secondsTillNextFlush;
368		#endregion
369
370		#region nextTimeFlushedInTicks (Private)
371		/// <summary>
372		/// The next time all cache data is flushed down the toilet is remembered
373		/// here and calculated in the constructor and once a flush happened.
374		/// </summary>
375		private long nextTimeFlushedInTicks;
376		#endregion
377
378		#region onlyCheckFlushEvery10Times (Private)
379		/// <summary>
380		/// Helper to make sure we only check for the flush time every 10 calls
381		/// to Get. Flushing is not so important to waste too much performance on
382		/// the constant calls (Stopwatch.GetTimestamp is pretty fast, but still).
383		/// Without this about 60-70% of performance is lost to
384		/// topwatch.GetTimestamp for simple gets, with this it is only ~20%.
385		/// </summary>
386		private int onlyCheckFlushEvery10Times;
387		#endregion
388
389		#endregion
390
391		#region Constructors
392		/// <summary>
393		/// Cache constructor, which will just set the time for the next flush.
394		/// </summary>
395		/// <param name="setSecondsTillNextFlush">Set time till next flush</param>
396		/// <param name="setCustomCode">Custom code to execute when a input
397		/// key is not found and the output value needs to be generated.</param>
398		public Cache(CustomCodeDelegate setCustomCode, int setSecondsTillNextFlush)
399		{
400			customCode = setCustomCode;
401			secondsTillNextFlush = setSecondsTillNextFlush;
402			nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
403			                         Stopwatch.Frequency * secondsTillNextFlush;
404		}
405
406		/// <summary>
407		/// Cache constructor with the default cache flush time of 10 minutes.
408		/// </summary>
409		/// <param name="setCustomCode">Custom code to execute when a input
410		/// key is not found and the output value needs to be generated.</param>
411		public Cache(CustomCodeDelegate setCustomCode)
412			: this(setCustomCode, DefaultFlushTimeInSeconds)
413		{
414		}
415		#endregion
416
417		#region Add (Public)
418		/// <summary>
419		/// Add a new input value with a specific output value, will update
420		/// an existing item if it already exists.
421		/// </summary>
422		/// <param name="input1">Input key part 1, must match</param>
423		/// <param name="input2">Input key part 2, must also match</param>
424		/// <param name="value">Value</param>
425		public void Add(TInput1 input1, TInput2 input2, TOutput value)
426		{
427			// Got no value yet for that input1?
428			if (data.ContainsKey(input1) == false)
429			{
430				// Then we need to create it in any case
431				Dictionary<TInput2, TOutput> innerData =
432					new Dictionary<TInput2, TOutput>();
433				innerData.Add(input2, value);
434				data.Add(input1, innerData);
435
436				// Note: Hopefully this is not slow, else only do this in DEBUG mode
437				if (data.Count > WarningTooMuchCacheEntries)
438				{
439					Log.Warning("Reached " + data.Count + " added cache entries. " +
440					            "This should not happen and the cache will be completely " +
441					            "cleared now to prevent performance impacts! " +
442					            "secondsTillNextFlush=" + secondsTillNextFlush);
443					Clear();
444				} // if (addedCacheEntries)
445			} // if (data.ContainsKey)
446			else
447			{
448				// First check if we also have the second input value
449				Dictionary<TInput2, TOutput> innerData = data[input1];
450				if (innerData.ContainsKey(input2) == false)
451				{
452					// Not found, then the inner data needs to be created.
453					innerData.Add(input2, value);
454				}
455				else
456				{
457					// Otherwise a simple update is fine. Note: Updates should
458					// not happen that often, the cache should be cleared instead.
459					innerData[input2] = value;
460				}
461			}
462		}
463		#endregion
464
465		#region Remove (Public)
466		/// <summary>
467		/// Remove cached value in case we updated something and want to clear a
468		/// specific cache entry. Note: Will not kill any dictionaries as data
469		/// will most likely be filled in soon. Use Clear to kill everything!
470		/// </summary>
471		/// <param name="input1">Input key part 1, must match</param>
472		/// <param name="input2">Input key part 2, must also match</param>
473		public void Remove(TInput1 input1, TInput2 input2)
474		{
475			if (data.ContainsKey(input1))
476			{
477				data[input1].Remove(input2);
478			}
479		}
480		#endregion
481
482		#region Clear (Public)
483		/// <summary>
484		/// Clear whole cache, updated lastTimeFlushed time and clear statistics
485		/// </summary>
486		public void Clear()
487		{
488			data.Clear();
489			nextTimeFlushedInTicks = Stopwatch.GetTimestamp() +
490			                         Stopwatch.Frequency * secondsTillNextFlush;
491
492			//Note: We could also call GC.Collect to free some memory.
493			//GC.Collect();
494		}
495		#endregion
496
497		#region Exists
498		/// <summary>
499		/// Check if an entry exists for the given input value. This will not get
500		/// the actual cached output value or create a new cached entry like the
501		/// Get method does.
502		/// </summary>
503		/// <param name="input1">First input key part</param>
504		/// <param name="input2">Second input key part</param>
505		/// <returns>True if the cache entry was found, false otherwise</returns>
506		public bool Exists(TInput1 input1, TInput2 input2)
507		{
508			// First check the outer list
509			Dictionary<TInput2, TOutput> innerData;
510			if (data.TryGetValue(input1, out innerData))
511			{
512				// And then do a simple ContainsKey check in the inner dictionary
513				return innerData.ContainsKey(input2);
514			}
515
516			// Otherwise the entry was not even found in the outer dictionary
517			return false;
518		}
519		#endregion
520
521		#region Get (Public)
522		/// <summary>
523		/// Get method, which gets the cached data, most important method here.
524		/// </summary>
525		/// <param name="input1">First input key part</param>
526		/// <param name="input2">Second input key part</param>
527		/// <returns>Output Type</returns>
528		public TOutput Get(TInput1 input1, TInput2 input2)
529		{
530			// Do we have to clear the whole cache because the data is too old?
531			if ((onlyCheckFlushEvery10Times++) % 10 == 0 &&
532			    Stopwatch.GetTimestamp() > nextTimeFlushedInTicks)
533			{
534				Clear();
535			}
536
537			// Check if already have that data cached!
538			TOutput ret;
539			Dictionary<TInput2, TOutput> innerData;
540			if (data.TryGetValue(input1, out innerData))
541			{
542				if (innerData.TryGetValue(input2, out ret))
543				{
544					return ret;
545				}
546			}
547
548			// Not cached? Then get data the slow way with the custom code delegate.
549			ret = customCode(input1, input2);
550			Add(input1, input2, ret);
551			return ret;
552		}
553		#endregion
554
555		#region GetAllEntries (Public)
556		/// <summary>
557		/// Return all entries as a flat list so we can easily enumerate though
558		/// them. Useful for disposing stuff.
559		/// </summary>
560		/// <returns>Flat list of TOutput</returns>
561		public IEnumerable<TOutput> GetAllEntries()
562		{
563			List<TOutput> ret = new List<TOutput>();
564			foreach (Dictionary<TInput2, TOutput> innerData in data.Values)
565			{
566				foreach (TOutput value in innerData.Values)
567				{
568					ret.Add(value);
569				}
570			}
571			return ret;
572		}
573		#endregion
574	}
575
576	#region CacheTests
577	/// <summary>
578	/// Tests for the generic Cache class (cannot do tests directly there).
579	/// </summary>
580	internal class CacheTests
581	{
582		#region Helpers
583		/// <summary>
584		/// Simple struct for the complexStructCache in TestPerformance.
585		/// </summary>
586		private struct SimpleStruct
587		{
588			#region X (Public)
589			public float X;
590			#endregion
591
592			#region Y (Public)
593			public float Y;
594			#endregion
595
596			#region Number (Public)
597			public int Number;
598			#endregion
599		}
600		#endregion
601
602		#region Helpers
603		/// <summary>
604		/// Helper struct for the input type of the ComplexInputAndOutputData test
605		/// </summary>
606		private struct StringAndNumber
607		{
608			#region Text (Public)
609			public string Text;
610			#endregion
611
612			#region Number (Public)
613			public int Number;
614			#endregion
615		}
616		#endregion
617
618		#region Helpers
619		/// <summary>
620		/// Helper struct for the output type of the ComplexInputAndOutputData test
621		/// </summary>
622		private struct LotsOfNumbers
623		{
624			#region Number1 (Public)
625			public int Number1;
626			#endregion
627
628			#region Number2 (Public)
629			public int Number2;
630			#endregion
631
632			#region Number3 (Public)
633			public int Number3;
634			#endregion
635		}
636		#endregion
637
638		#region TestPerformance (LongRunning)
639		/// <summary>
640		/// Performance test of the Cache class, doing 1 million gets.
641		/// <para />
642		/// Result is for 1 million iterations each (with a 4Ghz Sandy Bridge CPU):
643		/// <para />
644		/// simpleIntCache (int in, int out): ~20ms
645		/// <para />
646		/// stringLengthCache (string in, int out): ~35ms
647		/// <para />
648		/// complexCache (string in, float in, string out): ~55ms
649		/// <para />
650		/// complexStructCache (string in, Struct in, string out): ~140ms
651		/// <para />
652		/// structCache (struct with string+int in, struct out): ~1200ms
653		/// <para />
654		/// Note: The last one (structCache) is much slower than the rest and the
655		/// reason for this is the struct (string+int). Mixing string and other
656		/// datatypes is slow as hell when comparing structs, even the
657		/// complexStructCache version is slower than the rest with just 2 floats
658		/// and an integer number. All this need to be copied to the Get method
659		/// and then compared to all other dictionary values, which takes time!
660		/// Always use the generic Cache class with 2 inputs if you have a string
661		/// plus other data, and try to simplify the input key as much as possible.
662		/// </summary>
663		[Test]
664		[Category("LongRunning")]
665		public static void TestPerformance()
666		{
667			// Iterate for 1 million times.
668			const int Iterations = 1000000;
669
670			// Simple Cache<int, int> test (number in, number out, fastest)
671			Cache<int, int> simpleIntCache = new Cache<int, int>(
672				delegate(int input)
673				{
674					return input * 2;
675				});
676			Assert.Equal(4, simpleIntCache.Get(2));
677
678			// Now do it many times
679			long startTime = Stopwatch.GetTimestamp();
680			for (int num = 0; num < Iterations; num++)
681			{
682				simpleIntCache.Get(2);
683			}
684			long endTime = Stopwatch.GetTimestamp();
685			Log.Info(Iterations + " iterations of Cache<int, int>.Get took " +
686			         ((endTime - startTime) * 1000 / Stopwatch.Frequency) + "ms");
687
688			// Next test a Cache<string, int> version
689			Cache<string, int> stringLengthCache = new Cache<string, int>(
690				delegate(string input)
691				{
692					return input.Length;
693				});
694			Assert.Equal(9, stringLengthCache.Get("What's up"));
695
696			startTime = Stopwatch.GetTimestamp();
697			for (int num = 0; num < Iterations; num++)
698			{
699				stringLengthCache.Get("What's up");
700			}
701			endTime = Stopwatch.GetTimestamp();
702			Log.Info(Iterations + " iterations of Cache<string, int>.Get took " +
703			         ((endTime - startTime) * 1000 / Stopwatch.Frequency) + "ms");
704
705			// Test a more complex example with Cache<string, float, string>
706			Cache<string, float, string> complexCache =
707				new Cache<string, float, string>(
708					delegate(string inputText, float inputValue)
709					{
710						// Just build a pretty simple string from the input values
711						return inputText + " " + inputValue.ToInvariantString();
712					});
713			Assert.Equal("Hi there 5.2", complexCache.Get("Hi there", 5.2f));
714
715			startTime = Stopwatch.GetTimestamp();
716			for (int num = 0; num < Iterations; num++)
717			{
718				complexCache.Get("Hi there", 5.2f);
719			}
720			endTime = Stopwatch.GetTimestamp();
721			Log.Info(Iterations + " iterations of Cache<string, float, string>." +
722			         "Get took " + ((endTime - startTime) * 1000 / Stopwatch.Frequency) +
723			         "ms");
724
725			// Also test a complex example with struct
726			Cache<string, SimpleStruct, string> complexStructCache =
727				new Cache<string, SimpleStruct, string>(
728					delegate(string inputText, SimpleStruct inputStruct)
729					{
730						// Just build a pretty simple string from the input values
731						return inputText + " " +
732						       inputStruct.X.ToInvariantString() + ", " +
733						       inputStruct.Y.ToInvariantString() + ", " +
734						       inputStruct.Number;
735					});
736			Assert.Equal("Numbers: 1.2, 2.3, 9", complexStructCache.Get("Numbers:",
737				new SimpleStruct
738				{
739					X = 1.2f,
740					Y = 2.3f,
741					Number = 9,
742				}));
743
744			startTime = Stopwatch.GetTimestamp();
745			for (int num = 0; num < Iterations; num++)
746			{
747				complexStructCache.Get("Numbers:",
748					new SimpleStruct
749					{
750						X = 1.2f,
751						Y = 2.3f,
752						Number = 9,
753					});
754			}
755			endTime = Stopwatch.GetTimestamp();
756			Log.Info(Iterations + " iterations of Cache<string, SimpleStruct, " +
757			         "string>.Get took " + ((endTime - startTime) * 1000 /
758			                                Stopwatch.Frequency) + "ms");
759
760			// And finally test the slowest way with a complex struct, which should
761			// be avoided at all cost (the complex case using the 2 input key Cache
762			// above is much faster with almost the same kind of data).
763			Cache<StringAndNumber, LotsOfNumbers> structCache =
764				new Cache<StringAndNumber, LotsOfNumbers>(
765					delegate(StringAndNumber input)
766					{
767						return new LotsOfNumbers
768						{
769							Number1 = input.Number,
770							Number2 = input.Text.Length,
771							Number3 = input.Number + input.Text.Length,
772						};
773					});
774			Assert.Equal(5, structCache.Get(
775				new StringAndNumber
776				{
777					Text = "Hi",
778					Number = 5,
779				}).Number1);
780
781			// Now do it many times
782			startTime = Stopwatch.GetTimestamp();
783			for (int num = 0; num < Iterations; num++)
784			{
785				// Always use the same input data for quick performance test
786				structCache.Get(
787					// Still a new struct has to be constructed each time
788					new StringAndNumber
789					{
790						Text = "Hi",
791						Number = 5,
792					});
793			}
794			endTime = Stopwatch.GetTimestamp();
795			Log.Info(Iterations + " iterations of Cache<StringAndNumber, " +
796			         "LotsOfNumbers>.Get took " + ((endTime - startTime) * 1000 /
797			                                       Stopwatch.Frequency) +
798			         "ms (avoid this obviously)");
799		}
800		#endregion
801
802		#region GetCachedData
803		/// <summary>
804		/// Test get cached data
805		/// </summary>
806		[Test]
807		public void GetCachedData()
808		{
809			// Test very simple static cache with just one return value no matter
810			// what the input is.
811			Cache<int, string> hiCache = new Cache<int, string>(
812				delegate
813				{
814					return "Hi";
815				});
816			Assert.Equal("Hi", hiCache.Get(1));
817
818			// Next test a more complex example with many return values
819			Cache<string, int> getNumberCache = new Cache<string, int>(
820				delegate(string input)
821				{
822					switch (input)
823					{
824						case "One":
825							return 1;
826						case "Two":
827							return 2;
828						case "Ten":
829							return 10;
830						default:
831							return 0;
832					} // switch
833				});
834			Assert.Equal(1, getNumberCache.Get("One"));
835			// Try some more gets
836			Assert.Equal(2, getNumberCache.Get("Two"));
837			Assert.Equal(0, getNumberCache.Get("Something else"));
838			Assert.Equal(10, getNumberCache.Get("Ten"));
839
840			// Next up is a little delegate that just returns the length of a string
841			int numberOfTimeStringLengthCalled = 0;
842			Cache<string, int> stringLengthCache = new Cache<string, int>(
843				delegate(string input)
844				{
845					numberOfTimeStringLengthCalled++;
846					return input.Length;
847				});
848			Assert.Equal(3, stringLengthCache.Get("abc"));
849			Assert.Equal(2, stringLengthCache.Get("de"));
850			Assert.Equal(5, stringLengthCache.Get("benny"));
851			// Try getting a few more times
852			Assert.Equal(2, stringLengthCache.Get("de"));
853			Assert.Equal(5, stringLengthCache.Get("benny"));
854			// Overall the cache delegate should only have been called 3 times
855			Assert.Equal(3, numberOfTimeStringLengthCalled);
856		}
857		#endregion
858
859		// GetCachedData()
860
861		#region ClearCachedData
862		/// <summary>
863		/// Clear cached data
864		/// </summary>
865		[Test]
866		public void ClearCachedData()
867		{
868			int cacheGetCounter = 0;
869			Cache<int, int> doubleNumberCache = new Cache<int, int>(
870				delegate(int inputNumber)
871				{
872					cacheGetCounter++;
873					return inputNumber * 2;
874				});
875
876			// Do some gets, make sure each get is only executed once
877			Assert.Equal(0, cacheGetCounter);
878			Assert.Equal(4, doubleNumberCache.Get(2));
879			Assert.Equal(1, cacheGetCounter);
880			Assert.Equal(4, doubleNumberCache.Get(2));
881			Assert.Equal(1, cacheGetCounter);
882			// And call it with a different number
883			Assert.Equal(6, doubleNumberCache.Get(3));
884			Assert.Equal(2, cacheGetCounter);
885
886			// Delete a cached value and see if getting it again will execute
887			// doubleNumber once again.
888			doubleNumberCache.Clear();
889			Assert.Equal(2, cacheGetCounter);
890			Assert.Equal(4, doubleNumberCache.Get(2));
891			// Get counter should be increased now again
892			Assert.Equal(3, cacheGetCounter);
893			// And test some more gets, which should access cached data now again
894			Assert.Equal(4, doubleNumberCache.Get(2));
895			Assert.Equal(4, doubleNumberCache.Get(2));
896			Assert.Equal(3, cacheGetCounter);
897		}
898		#endregion
899
900		#region MultipleInputKeys
901		/// <summary>
902		/// Unit test to show of a more complex example with multiple input keys.
903		/// </summary>
904		[Test]
905		public void MultipleInputKeys()
906		{
907			Cache<string, int, LotsOfNumbers> multipleKeysCache =
908				new Cache<string, int, LotsOfNumbers>(
909					delegate(string inputText, int inputNumber)
910					{
911						return new LotsOfNumbers
912						{
913							Number1 = inputNumber,
914							//Note: Intentionally wrong code to test if more delegates are
915							// generated then there should be (5 instead of 1)!
916							//Number2 = text.Length,
917							// And this is the correct code!
918							Number2 = inputText.Length,
919							Number3 = inputNumber + inputText.Length,
920						};
921					});
922
923			Assert.Equal(1, multipleKeysCache.Get("", 1).Number1);
924			Assert.Equal(0, multipleKeysCache.Get("", 1).Number2);
925			Assert.Equal(5, multipleKeysCache.Get("Hi", 3).Number3);
926			Assert.Equal(9, multipleKeysCache.Get("Whats up", 1).Number3);
927		}
928		#endregion
929	}
930	#endregion
931}