PageRenderTime 48ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/Utilities/Cache.cs

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