1   /**
2    *  Copyright 2003-2007 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.store;
18  
19  import net.sf.ehcache.AbstractCacheTest;
20  import net.sf.ehcache.Element;
21  import net.sf.ehcache.MemoryStoreTester;
22  import net.sf.ehcache.StopWatch;
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  
26  import java.io.IOException;
27  import java.util.Date;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.Map;
31  import java.util.Random;
32  
33  /**
34   * Test class for LfuMemoryStore
35   * <p/>
36   * @author <a href="ssuravarapu@users.sourceforge.net">Surya Suravarapu</a>
37   * @version $Id: LfuMemoryStoreTest.java 512 2007-07-10 09:18:45Z gregluck $
38   */
39  public class LfuMemoryStoreTest extends MemoryStoreTester {
40  
41      private static final Log LOG = LogFactory.getLog(LfuMemoryStoreTest.class.getName());
42  
43      /**
44       * setup test
45       */
46      protected void setUp() throws Exception {
47          super.setUp();
48          createMemoryStore(MemoryStoreEvictionPolicy.LFU);
49      }
50  
51  
52      /**
53       * Tests the put by reading the config file
54       */
55      public void testPutFromConfig() throws Exception {
56          createMemoryStore(AbstractCacheTest.TEST_CONFIG_DIR + "ehcache-policy-test.xml", "sampleLFUCache1");
57          putTest();
58      }
59  
60      /**
61       * Tests the put by reading the config file
62       */
63      public void testPutFromConfigZeroMemoryStore() throws Exception {
64          createMemoryStore(AbstractCacheTest.TEST_CONFIG_DIR + "ehcache-policy-test.xml", "sampleLFUCache2");
65          Element element = new Element("1", "value");
66          store.put(element);
67          assertNull(store.get("1"));
68      }
69  
70      /**
71       * Tests the remove() method by using the parameters specified in the config file
72       */
73      public void testRemoveFromConfig() throws Exception {
74          createMemoryStore(AbstractCacheTest.TEST_CONFIG_DIR + "ehcache-policy-test.xml", "sampleLFUCache1");
75          removeTest();
76      }
77  
78      /**
79       * Benchmark to test speed.
80       * This takes a little longer for LFU than the others.
81       * Takes about 7400ms
82       */
83      public void testBenchmarkPutGetSurya() throws Exception {
84          benchmarkPutGetSuryaTest(9000);
85      }
86  
87      /**
88       * Tests the LFU policy
89       */
90      public void testLfuPolicy() throws Exception {
91          createMemoryStore(MemoryStoreEvictionPolicy.LFU, 4);
92          lfuPolicyTest();
93      }
94  
95      /**
96       * Tests the LFU policy by using the parameters specified in the config file
97       */
98      public void testLfuPolicyFromConfig() throws Exception {
99          createMemoryStore(AbstractCacheTest.TEST_CONFIG_DIR + "ehcache-policy-test.xml", "sampleLFUCache1");
100         lfuPolicyTest();
101     }
102 
103 
104     private void lfuPolicyTest() throws IOException {
105         //Make sure that the store is empty to start with
106         assertEquals(0, store.getSize());
107 
108         // Populate the store till the max limit
109         Element element = new Element("key1", "value1");
110         store.put(element);
111         assertEquals(1, store.getSize());
112 
113         element = new Element("key2", "value2");
114         store.put(element);
115         assertEquals(2, store.getSize());
116 
117         element = new Element("key3", "value3");
118         store.put(element);
119         assertEquals(3, store.getSize());
120 
121         element = new Element("key4", "value4");
122         store.put(element);
123         assertEquals(4, store.getSize());
124 
125         //Now access the elements to boost the hit count
126         store.get("key1");
127         store.get("key1");
128         store.get("key3");
129         store.get("key3");
130         store.get("key3");
131         store.get("key4");
132 
133         //Create a new element and put in the store so as to force the policy
134         element = new Element("key5", "value5");
135         store.put(element);
136 
137         assertEquals(4, store.getSize());
138         //The element with key "key2" is the LFU element so should be removed
139         assertNull(store.get("key2"));
140 
141         // Make some more accesses
142         store.get("key5");
143         store.get("key5");
144 
145         // Insert another element to force the policy
146         element = new Element("key6", "value6");
147         store.put(element);
148         assertEquals(4, store.getSize());
149         assertNull(store.get("key4"));
150 
151 
152 
153     }
154 
155     /**
156      * Benchmark to test speed.
157      * new sampling LFU 417ms
158      */
159     public void testBenchmarkPutGetRemove() throws Exception {
160         super.testBenchmarkPutGetRemove();
161     }
162 
163 
164     /**
165      * Multi-thread read, put and removeAll test.
166      * This checks for memory leaks
167      * using the removeAll which was the known cause of memory leaks with LruMemoryStore in JCS
168      * new sampling LFU has no leaks
169      */
170     public void testMemoryLeak() throws Exception {
171         super.testMemoryLeak();
172     }
173 
174     /**
175      * Benchmark to test speed.
176      * new sampling LFU 132ms
177      */
178     public void testBenchmarkPutGet() throws Exception {
179         super.testBenchmarkPutGet();
180     }
181 
182     /**
183      * Tests how random the java.util.Map iteration is by measuring the differences in iterate order.
184      * <p/>
185      * If iterate was ordered in either insert or reverse insert order the mean difference would be 1.
186      * Using Random gives a mean difference of 343.
187      * The observed value is 175 but always 175 for a key set of 500 because it always iterates in the same order,
188      * just not an obvious order.
189      * <p/>
190      * Conclusion: Unable to use the iterator as a pseudorandom selector.
191      */
192     public void testRandomnessOfIterator() {
193         int mean = 0;
194         int absoluteDifferences = 0;
195         int lastReading = 0;
196         Map map = new HashMap();
197         for (int i = 1; i <= 500; i++) {
198             mean += i;
199             map.put("" + i, " ");
200         }
201         mean = mean / 500;
202         for (Iterator iterator = map.keySet().iterator(); iterator.hasNext();) {
203             String string = (String) iterator.next();
204             int thisReading = Integer.parseInt(string);
205             absoluteDifferences += Math.abs(lastReading - thisReading);
206             lastReading = thisReading;
207         }
208         LOG.info("Mean difference through iteration: " + absoluteDifferences / 500);
209 
210         //Random selection without replacement
211         Random random = new Random();
212         while (map.size() != 0) {
213             int thisReading = random.nextInt(501);
214             Object o = map.remove("" + thisReading);
215             if (o == null) {
216                 continue;
217             }
218             absoluteDifferences += Math.abs(lastReading - thisReading);
219             lastReading = thisReading;
220         }
221         LOG.info("Mean difference with random selection without replacement : " + absoluteDifferences / 500);
222         LOG.info("Mean of range 1 - 500 : " + mean);
223 
224     }
225 
226 
227     /**
228      * Check nothing breaks and that we get the right number of samples
229      *
230      * @throws IOException
231      */
232     public void testSampling() throws IOException {
233         createMemoryStore(MemoryStoreEvictionPolicy.LFU, 1000);
234         LfuPolicy.Metadata[] elements = null;
235         for (int i = 0; i < 10; i++) {
236             store.put(new Element("" + i, new Date()));
237             elements = ((LfuMemoryStore) store).sampleElements(i + 1);
238         }
239 
240         for (int i = 10; i < 2000; i++) {
241             store.put(new Element("" + i, new Date()));
242             elements = ((LfuMemoryStore) store).sampleElements(10);
243             assertEquals(10, elements.length);
244         }
245     }
246 
247 
248     /**
249      * Check we get reasonable results for 2000 entries where entry 0 is accessed once increasing to entry 1999 accessed
250      * 2000 times.
251      * <p/>
252      * 1 to 5000 population, with hit counts ranging from 1 to 500, not selecting lowest half. 5000 tests
253      * S  Cost  No
254      * 7        38 99.24% confidence
255      * 8        27 99.46% confidence
256      * 9        10
257      * 10 11300 4  99.92% confidence
258      * 12       2
259      * 20 11428 0  99.99% confidence
260      * <p/>
261      * 1 to 5000 population, with hit counts ranging from 1 to 500, not selecting lowest quarter. 5000 tests
262      * S        No
263      * 10       291 94.18% confidence
264      * 20       15
265      * 30       11536 1 99.99% confidence
266      * <p/>
267      * For those with a statistical background the branch of stats which deals with this is hypothesis testing and
268      * the Student's T distribution. The higher your sample the greater confidence you can have in a hypothesis, in
269      * this case whether or not the "lowest" value lies in the bottom half or quarter of the distribution. Adding
270      * samples rapidly increases confidence but the return from extra sampling rapidly diminishes.
271      * <p/>
272      * Cost is not affected much by sample size. Profiling shows that it is the iteration that is causing most of the
273      * time. If we had access to the array backing Map, all would work very fast. Still, it is fast enough.
274      * <p/>
275      * A 99.99% confidence interval can be achieved that the "lowest" element is actually in the bottom quarter of the
276      * hit count distribution.
277      * @throws IOException
278      * Performance:
279      * With a sample size of 10: 523ms for 5000 runs = 104 ?s per run
280      * With a sample size of 30: 628ms for 5000 runs = 125 ?s per run
281      */
282     public void testLowest() throws IOException {
283         createMemoryStore(MemoryStoreEvictionPolicy.LFU, 5000);
284         Element element = null;
285         Element newElement = null;
286         for (int i = 0; i < 10; i++) {
287             newElement = new Element("" + i, new Date());
288             store.put(newElement);
289             int j;
290             for (j = 0; j <= i; j++) {
291                 store.get("" + i);
292             }
293             if (i > 0) {
294                 element = ((LfuMemoryStore) store).findRelativelyUnused(newElement);
295                 assertTrue(!element.equals(newElement));
296                 assertTrue(element.getHitCount() < 2);
297             }
298         }
299 
300         int lowestQuarterNotIdentified = 0;
301 
302         long findTime = 0;
303         StopWatch stopWatch = new StopWatch();
304         for (int i = 10; i < 5000; i++) {
305             store.put(new Element("" + i, new Date()));
306             int j;
307             int maximumHitCount = 0;
308             for (j = 0; j <= i; j += 10) {
309                 store.get("" + i);
310                 maximumHitCount++;
311             }
312 
313             stopWatch.getElapsedTime();
314             element = ((LfuMemoryStore) store).findRelativelyUnused(newElement);
315             findTime  += stopWatch.getElapsedTime();
316             long lowest = element.getHitCount();
317             long bottomQuarter = (Math.round(maximumHitCount / 4.0) + 1);
318             assertTrue(!element.equals(newElement));
319             if (lowest > bottomQuarter) {
320                 lowestQuarterNotIdentified++;
321                 //LOG.info(i + " " + maximumHitCount + " " + element);
322             }
323         }
324         LOG.info("Find time: " + findTime);
325         assertTrue(findTime < 1000);
326         LOG.info("Selections not in lowest quartile: " + lowestQuarterNotIdentified);
327         assertTrue(lowestQuarterNotIdentified < 5);
328 
329     }
330 
331 }