blob: de6181810db08f9290566cd0cd6523669eba32bb [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.util;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Random;
import org.junit.Assert;
import org.junit.Test;
/** Testing {@link LightWeightCache} */
public class TestLightWeightCache {
private static final long starttime = Time.now();
private static final long seed = starttime;
private static final Random ran = new Random(seed);
static {
println("Start time = " + new Date(starttime) + ", seed=" + seed);
}
private static void print(Object s) {
System.out.print(s);
System.out.flush();
}
private static void println(Object s) {
System.out.println(s);
}
@Test
public void testLightWeightCache() {
// test randomized creation expiration with zero access expiration
{
final long creationExpiration = ran.nextInt(1024) + 1;
check(1, creationExpiration, 0L, 1 << 10, 65537);
check(17, creationExpiration, 0L, 1 << 16, 17);
check(255, creationExpiration, 0L, 1 << 16, 65537);
}
// test randomized creation/access expiration periods
for(int i = 0; i < 3; i++) {
final long creationExpiration = ran.nextInt(1024) + 1;
final long accessExpiration = ran.nextInt(1024) + 1;
check(1, creationExpiration, accessExpiration, 1 << 10, 65537);
check(17, creationExpiration, accessExpiration, 1 << 16, 17);
check(255, creationExpiration, accessExpiration, 1 << 16, 65537);
}
// test size limit
final int dataSize = 1 << 16;
for(int i = 0; i < 10; i++) {
final int modulus = ran.nextInt(1024) + 1;
final int sizeLimit = ran.nextInt(modulus) + 1;
checkSizeLimit(sizeLimit, dataSize, modulus);
}
}
private static void checkSizeLimit(final int sizeLimit, final int datasize,
final int modulus) {
final LightWeightCacheTestCase test = new LightWeightCacheTestCase(
sizeLimit, sizeLimit, 1L << 32, 1L << 32, datasize, modulus);
// keep putting entries and check size limit
print(" check size ................. ");
for(int i = 0; i < test.data.size(); i++) {
test.cache.put(test.data.get(i));
Assert.assertTrue(test.cache.size() <= sizeLimit);
}
println("DONE " + test.stat());
}
/**
* Test various createionExpirationPeriod and accessExpirationPeriod.
* It runs ~2 minutes. If you are changing the implementation,
* please un-comment the following line in order to run the test.
*/
// @Test
public void testExpirationPeriods() {
for(int k = -4; k < 10; k += 4) {
final long accessExpirationPeriod = k < 0? 0L: (1L << k);
for(int j = 0; j < 10; j += 4) {
final long creationExpirationPeriod = 1L << j;
runTests(1, creationExpirationPeriod, accessExpirationPeriod);
for(int i = 1; i < Integer.SIZE - 1; i += 8) {
runTests((1 << i) + 1, creationExpirationPeriod, accessExpirationPeriod);
}
}
}
}
/** Run tests with various table lengths. */
private static void runTests(final int modulus,
final long creationExpirationPeriod,
final long accessExpirationPeriod) {
println("\n\n\n*** runTest: modulus=" + modulus
+ ", creationExpirationPeriod=" + creationExpirationPeriod
+ ", accessExpirationPeriod=" + accessExpirationPeriod);
for(int i = 0; i <= 16; i += 4) {
final int tablelength = (1 << i);
final int upper = i + 2;
final int steps = Math.max(1, upper/3);
for(int j = upper; j > 0; j -= steps) {
final int datasize = 1 << j;
check(tablelength, creationExpirationPeriod, accessExpirationPeriod,
datasize, modulus);
}
}
}
private static void check(int tablelength, long creationExpirationPeriod,
long accessExpirationPeriod, int datasize, int modulus) {
check(new LightWeightCacheTestCase(tablelength, -1,
creationExpirationPeriod, accessExpirationPeriod, datasize, modulus));
}
/**
* check the following operations
* (1) put
* (2) remove & put
* (3) remove
* (4) remove & put again
*/
private static void check(final LightWeightCacheTestCase test) {
//check put
print(" check put .................. ");
for(int i = 0; i < test.data.size()/2; i++) {
test.put(test.data.get(i));
}
for(int i = 0; i < test.data.size(); i++) {
test.put(test.data.get(i));
}
println("DONE " + test.stat());
//check remove and put
print(" check remove & put ......... ");
for(int j = 0; j < 10; j++) {
for(int i = 0; i < test.data.size()/2; i++) {
final int r = ran.nextInt(test.data.size());
test.remove(test.data.get(r));
}
for(int i = 0; i < test.data.size()/2; i++) {
final int r = ran.nextInt(test.data.size());
test.put(test.data.get(r));
}
}
println("DONE " + test.stat());
//check remove
print(" check remove ............... ");
for(int i = 0; i < test.data.size(); i++) {
test.remove(test.data.get(i));
}
Assert.assertEquals(0, test.cache.size());
println("DONE " + test.stat());
//check remove and put again
print(" check remove & put again ... ");
for(int j = 0; j < 10; j++) {
for(int i = 0; i < test.data.size()/2; i++) {
final int r = ran.nextInt(test.data.size());
test.remove(test.data.get(r));
}
for(int i = 0; i < test.data.size()/2; i++) {
final int r = ran.nextInt(test.data.size());
test.put(test.data.get(r));
}
}
println("DONE " + test.stat());
final long s = (Time.now() - starttime)/1000L;
println("total time elapsed=" + s + "s\n");
}
/**
* The test case contains two data structures, a cache and a hashMap.
* The hashMap is used to verify the correctness of the cache. Note that
* no automatic eviction is performed in the hashMap. Thus, we have
* (1) If an entry exists in cache, it MUST exist in the hashMap.
* (2) If an entry does not exist in the cache, it may or may not exist in the
* hashMap. If it exists, it must be expired.
*/
private static class LightWeightCacheTestCase implements GSet<IntEntry, IntEntry> {
/** hashMap will not evict entries automatically. */
final GSet<IntEntry, IntEntry> hashMap
= new GSetByHashMap<IntEntry, IntEntry>(1024, 0.75f);
final LightWeightCache<IntEntry, IntEntry> cache;
final IntData data;
final String info;
final long starttime = Time.now();
/** Determine the probability in {@link #check()}. */
final int denominator;
int iterate_count = 0;
int contain_count = 0;
private FakeTimer fakeTimer = new FakeTimer();
LightWeightCacheTestCase(int tablelength, int sizeLimit,
long creationExpirationPeriod, long accessExpirationPeriod,
int datasize, int modulus) {
denominator = Math.min((datasize >> 7) + 1, 1 << 16);
info = getClass().getSimpleName() + "(" + new Date(starttime)
+ "): tablelength=" + tablelength
+ ", creationExpirationPeriod=" + creationExpirationPeriod
+ ", accessExpirationPeriod=" + accessExpirationPeriod
+ ", datasize=" + datasize
+ ", modulus=" + modulus
+ ", denominator=" + denominator;
println(info);
data = new IntData(datasize, modulus);
cache = new LightWeightCache<IntEntry, IntEntry>(tablelength, sizeLimit,
creationExpirationPeriod, 0, fakeTimer);
Assert.assertEquals(0, cache.size());
}
private boolean containsTest(IntEntry key) {
final boolean c = cache.contains(key);
if (c) {
Assert.assertTrue(hashMap.contains(key));
} else {
final IntEntry h = hashMap.remove(key);
if (h != null) {
Assert.assertTrue(cache.isExpired(h, fakeTimer.monotonicNowNanos()));
}
}
return c;
}
@Override
public boolean contains(IntEntry key) {
final boolean e = containsTest(key);
check();
return e;
}
private IntEntry getTest(IntEntry key) {
final IntEntry c = cache.get(key);
if (c != null) {
Assert.assertEquals(hashMap.get(key).id, c.id);
} else {
final IntEntry h = hashMap.remove(key);
if (h != null) {
Assert.assertTrue(cache.isExpired(h, fakeTimer.monotonicNowNanos()));
}
}
return c;
}
@Override
public IntEntry get(IntEntry key) {
final IntEntry e = getTest(key);
check();
return e;
}
private IntEntry putTest(IntEntry entry) {
final IntEntry c = cache.put(entry);
if (c != null) {
Assert.assertEquals(hashMap.put(entry).id, c.id);
} else {
final IntEntry h = hashMap.put(entry);
if (h != null && h != entry) {
// if h == entry, its expiration time is already updated
Assert.assertTrue(cache.isExpired(h, fakeTimer.monotonicNowNanos()));
}
}
return c;
}
@Override
public IntEntry put(IntEntry entry) {
final IntEntry e = putTest(entry);
check();
return e;
}
private IntEntry removeTest(IntEntry key) {
final IntEntry c = cache.remove(key);
if (c != null) {
Assert.assertEquals(c.id, hashMap.remove(key).id);
} else {
final IntEntry h = hashMap.remove(key);
if (h != null) {
Assert.assertTrue(cache.isExpired(h, fakeTimer.monotonicNowNanos()));
}
}
return c;
}
@Override
public IntEntry remove(IntEntry key) {
final IntEntry e = removeTest(key);
check();
return e;
}
private int sizeTest() {
final int c = cache.size();
Assert.assertTrue(hashMap.size() >= c);
return c;
}
@Override
public int size() {
final int s = sizeTest();
check();
return s;
}
@Override
public Iterator<IntEntry> iterator() {
throw new UnsupportedOperationException();
}
boolean tossCoin() {
return ran.nextInt(denominator) == 0;
}
void check() {
fakeTimer.advanceNanos(ran.nextInt() & 0x3);
//test size
sizeTest();
if (tossCoin()) {
//test get(..), check content and test iterator
iterate_count++;
for(IntEntry i : cache) {
getTest(i);
}
}
if (tossCoin()) {
//test contains(..)
contain_count++;
final int count = Math.min(data.size(), 1000);
if (count == data.size()) {
for(IntEntry i : data.integers) {
containsTest(i);
}
} else {
for(int j = 0; j < count; j++) {
containsTest(data.get(ran.nextInt(data.size())));
}
}
}
}
String stat() {
final long t = Time.now() - starttime;
return String.format(" iterate=%5d, contain=%5d, time elapsed=%5d.%03ds",
iterate_count, contain_count, t/1000, t%1000);
}
@Override
public void clear() {
hashMap.clear();
cache.clear();
Assert.assertEquals(0, size());
}
@Override
public Collection<IntEntry> values() {
throw new UnsupportedOperationException();
}
}
private static class IntData {
final IntEntry[] integers;
IntData(int size, int modulus) {
integers = new IntEntry[size];
for(int i = 0; i < integers.length; i++) {
integers[i] = new IntEntry(i, ran.nextInt(modulus));
}
}
IntEntry get(int i) {
return integers[i];
}
int size() {
return integers.length;
}
}
/** Entries of {@link LightWeightCache} in this test */
private static class IntEntry implements LightWeightCache.Entry,
Comparable<IntEntry> {
private LightWeightGSet.LinkedElement next;
final int id;
final int value;
private long expirationTime = 0;
IntEntry(int id, int value) {
this.id = id;
this.value = value;
}
@Override
public boolean equals(Object obj) {
return obj != null && obj instanceof IntEntry
&& value == ((IntEntry)obj).value;
}
@Override
public int hashCode() {
return value;
}
@Override
public int compareTo(IntEntry that) {
return value - that.value;
}
@Override
public String toString() {
return id + "#" + value + ",expirationTime=" + expirationTime;
}
@Override
public LightWeightGSet.LinkedElement getNext() {
return next;
}
@Override
public void setNext(LightWeightGSet.LinkedElement e) {
next = e;
}
@Override
public void setExpirationTime(long timeNano) {
this.expirationTime = timeNano;
}
@Override
public long getExpirationTime() {
return expirationTime;
}
}
}