blob: a154858458ac47ba8f01430d620b832fe0ea8790 [file] [log] [blame]
package com.gemstone.gemfire.internal.offheap;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import static org.junit.Assert.*;
import junit.framework.TestCase;
import com.gemstone.gemfire.internal.offheap.SimpleMemoryAllocatorImpl.Chunk;
import com.gemstone.gemfire.test.junit.categories.UnitTest;
/**
* Tests fill pattern validation for the {@link SimpleMemoryAllocatorImpl}.
* @author rholmes
*/
@Category(UnitTest.class)
public class SimpleMemoryAllocatorFillPatternJUnitTest {
/**
* Chunk operation types.
* @author rholmes
*/
static enum Operation {
ALLOCATE,
FREE,
WRITE;
// Unfortunately we cannot use ThreadLocalRandom in order to maintain 1.6 compatibility...
private static Random random = new Random(System.currentTimeMillis());
// Holds all Operation values
private static Operation[] values = Operation.values();
static Operation randomOperation() {
return values[random.nextInt(values.length)];
}
};
/** Number of worker threads for advanced tests. */
private static final int WORKER_THREAD_COUNT = 5;
/** Size of single test slab.*/
private static final int SLAB_SIZE = 1024 * 1024 * 50;
/** Maximum number of bytes a worker thread can allocate during advanced tests. */
private static final int MAX_WORKER_ALLOCATION_TOTAL_SIZE = SLAB_SIZE / WORKER_THREAD_COUNT / 2;
/** Maximum allocation for a single Chunk. */
private static final int MAX_WORKER_ALLOCATION_SIZE = 512;
/** Canned data for write operations. */
private static final byte[] WRITE_BYTES = new String("Some string data.").getBytes();
/** Minimum size for write operations. */
private static final int MIN_WORKER_ALLOCATION_SIZE = WRITE_BYTES.length;
/** Runtime for worker threads. */
private static final long RUN_TIME_IN_MILLIS = 1 * 1000 * 60;
/** Chunk size for basic huge allocation test. */
private static final int HUGE_CHUNK_SIZE = 1024 * 200;
/** The number of chunks to allocate in order to force compaction. */
private static final int COMPACTION_CHUNKS = 3;
/** Our slab size divided in three (with some padding for safety). */
private static final int COMPACTION_CHUNK_SIZE = (SLAB_SIZE / COMPACTION_CHUNKS) - 1024;
/** This should force compaction when allocated. */
private static final int FORCE_COMPACTION_CHUNK_SIZE = COMPACTION_CHUNK_SIZE * 2;
/** Our test victim. */
private SimpleMemoryAllocatorImpl allocator = null;
/** Our test victim's memory slab. */
private UnsafeMemoryChunk slab = null;
/**
* Enables fill validation and creates the test victim.
*/
@Before
public void setUp() throws Exception {
System.setProperty("gemfire.validateOffHeapWithFill", "true");
this.slab = new UnsafeMemoryChunk(SLAB_SIZE);
this.allocator = SimpleMemoryAllocatorImpl.create(new NullOutOfOffHeapMemoryListener(), new NullOffHeapMemoryStats(), new UnsafeMemoryChunk[]{this.slab});
}
/**
* Frees off heap memory.
*/
@After
public void tearDown() throws Exception {
SimpleMemoryAllocatorImpl.freeOffHeapMemory();
System.clearProperty("gemfire.validateOffHeapWithFill");
}
/**
* This tests the fill pattern for a single tiny Chunk allocation.
* @throws Exception
*/
@Test
public void testFillPatternBasicForTinyAllocations() throws Exception {
/*
* Pull a chunk off the fragment. This will have no fill because
* it is a "fresh" chunk.
*/
Chunk chunk = (Chunk) this.allocator.allocate(1024, null);
/*
* Chunk should have valid fill from initial fragment allocation.
*/
try {
chunk.validateFill();
} catch(IllegalStateException e) {
fail(e.getMessage());
}
// "Dirty" the chunk so the release has something to fill over
chunk.writeBytes(Chunk.MIN_CHUNK_SIZE + 1, WRITE_BYTES);
// This should free the Chunk (ref count == 1)
chunk.release();
/*
* This chunk should have a fill because it was reused from the
* free list (assuming no fragmentation at this point...)
*/
chunk = (Chunk) this.allocator.allocate(1024, null);
// Make sure we have a fill this time
try {
chunk.validateFill();
} catch(IllegalStateException e) {
TestCase.fail("Chunk fill validation failed: " + e.getMessage());
}
// Give the fill code something to write over during the release
chunk.writeBytes(Chunk.MIN_CHUNK_SIZE + 1, WRITE_BYTES);
chunk.release();
// Again, make sure the release implemented the fill
try {
chunk.validateFill();
} catch(IllegalStateException e) {
TestCase.fail("Chunk fill validation failed: " + e.getMessage());
}
// "Dirty up" the free chunk
chunk.writeBytes(Chunk.MIN_CHUNK_SIZE + 1, WRITE_BYTES);
boolean failure = false;
// One final check for validateFill()
try {
chunk.validateFill();
} catch(IllegalStateException e) {
failure = true;
}
assertTrue(failure);
}
/**
* This tests the fill pattern for a single huge Chunk allocation.
* @throws Exception
*/
@Test
public void testFillPatternBasicForHugeAllocations() throws Exception {
/*
* Pull a chunk off the fragment. This will have no fill because
* it is a "fresh" chunk.
*/
Chunk chunk = (Chunk) this.allocator.allocate(HUGE_CHUNK_SIZE, null);
/*
* Chunk should have valid fill from initial fragment allocation.
*/
try {
chunk.validateFill();
} catch(IllegalStateException e) {
fail(e.getMessage());
}
// "Dirty" the chunk so the release has something to fill over
chunk.writeBytes(Chunk.MIN_CHUNK_SIZE + 1, WRITE_BYTES);
// This should free the Chunk (ref count == 1)
chunk.release();
/*
* This chunk should have a fill because it was reused from the
* free list (assuming no fragmentation at this point...)
*/
chunk = (Chunk) this.allocator.allocate(HUGE_CHUNK_SIZE, null);
// Make sure we have a fill this time
try {
chunk.validateFill();
} catch(IllegalStateException e) {
TestCase.fail("Chunk fill validation failed: " + e.getMessage());
}
// Give the fill code something to write over during the release
chunk.writeBytes(Chunk.MIN_CHUNK_SIZE + 1, WRITE_BYTES);
chunk.release();
// Again, make sure the release implemented the fill
try {
chunk.validateFill();
} catch(IllegalStateException e) {
TestCase.fail("Chunk fill validation failed: " + e.getMessage());
}
// "Dirty up" the free chunk
chunk.writeBytes(Chunk.MIN_CHUNK_SIZE + 1, WRITE_BYTES);
boolean failure = false;
// One final check for validateFill()
try {
chunk.validateFill();
} catch(IllegalStateException e) {
failure = true;
}
assertTrue(failure);
}
/**
* This test hammers a SimpleMemoryAllocatorImpl with multiple threads exercising
* the fill validation of tiny Chunks for one minute. This, of course, exercises many aspects of
* the SimpleMemoryAllocatorImpl and its helper classes.
* @throws Exception
*/
@Test
public void testFillPatternAdvancedForTinyAllocations() throws Exception {
// Used to manage worker thread completion
final CountDownLatch latch = new CountDownLatch(WORKER_THREAD_COUNT);
// Use to track any errors the worker threads will encounter
final List<Throwable> threadErrorList = Collections.synchronizedList(new LinkedList<Throwable>());
/*
* Start up a number of worker threads. These threads will randomly allocate, free,
* and write to Chunks.
*/
for(int i = 0;i < WORKER_THREAD_COUNT;++i) {
new Thread(new Runnable() {
// Total allocation in bytes for this thread
private int totalAllocation = 0;
// List of Chunks allocated by this thread
private List<Chunk> chunks = new LinkedList<Chunk>();
// Time to end thread execution
private long endTime = System.currentTimeMillis() + RUN_TIME_IN_MILLIS;
// Randomizer used for random Chunk size allocation
private Random random = new Random(endTime);
/**
* Returns an allocation size between a min and max constraint.
*/
private int allocationSize() {
int allocation = random.nextInt(MAX_WORKER_ALLOCATION_SIZE+1);
while(allocation < MIN_WORKER_ALLOCATION_SIZE) {
allocation = random.nextInt(MAX_WORKER_ALLOCATION_SIZE+1);
}
return allocation;
}
/**
* Allocates a chunk and adds it to the thread's Chunk list.
*/
private void allocate() {
int allocation = allocationSize();
Chunk chunk = (Chunk) allocator.allocate(allocation, null);
// This should always work just after allocation
chunk.validateFill();
chunks.add(chunk);
totalAllocation += chunk.getSize();
}
/**
* Frees a random chunk from the Chunk list.
*/
private void free() {
Chunk chunk = chunks.remove(random.nextInt(chunks.size()));
totalAllocation -= chunk.getSize();
/*
* Chunk is filled here but another thread may have already grabbed it so we
* cannot validate the fill.
*/
chunk.release();
}
/**
* Writes canned data to a random Chunk from the Chunk list.
*/
private void write() {
Chunk chunk = chunks.get(random.nextInt(chunks.size()));
chunk.writeBytes(0, WRITE_BYTES);
}
/**
* Randomly selects Chunk operations and executes them
* for a period of time. Collects any error thrown during execution.
*/
@Override
public void run() {
try {
for(long currentTime = System.currentTimeMillis();currentTime < endTime;currentTime = System.currentTimeMillis()) {
Operation op = (totalAllocation == 0 ? Operation.ALLOCATE : (totalAllocation >= MAX_WORKER_ALLOCATION_TOTAL_SIZE ? Operation.FREE : Operation.randomOperation()));
switch(op) {
case ALLOCATE:
allocate();
break;
case FREE:
free();
break;
case WRITE:
write();
break;
}
}
} catch (Throwable t) {
threadErrorList.add(t);
} finally {
latch.countDown();
}
}
}).start();
}
// Make sure each thread ended cleanly
assertTrue(latch.await(2, TimeUnit.MINUTES));
// Fail on the first error we find
if(!threadErrorList.isEmpty()) {
fail(threadErrorList.get(0).getMessage());
}
}
/**
* This test hammers a SimpleMemoryAllocatorImpl with multiple threads exercising
* the fill validation of huge Chunks for one minute. This, of course, exercises many aspects of
* the SimpleMemoryAllocatorImpl and its helper classes.
* @throws Exception
*/
@Test
public void testFillPatternAdvancedForHugeAllocations() throws Exception {
// Used to manage worker thread completion
final CountDownLatch latch = new CountDownLatch(WORKER_THREAD_COUNT);
// Use to track any errors the worker threads will encounter
final List<Throwable> threadErrorList = Collections.synchronizedList(new LinkedList<Throwable>());
/*
* Start up a number of worker threads. These threads will randomly allocate, free,
* and write to Chunks.
*/
for(int i = 0;i < WORKER_THREAD_COUNT;++i) {
new Thread(new Runnable() {
// Total allocation in bytes for this thread
private int totalAllocation = 0;
// List of Chunks allocated by this thread
private List<Chunk> chunks = new LinkedList<Chunk>();
// Time to end thread execution
private long endTime = System.currentTimeMillis() + RUN_TIME_IN_MILLIS;
// Randomizer used for random Chunk size allocation
private Random random = new Random(endTime);
/**
* Returns an allocation size between a min and max constraint.
*/
private int allocationSize() {
return HUGE_CHUNK_SIZE;
}
/**
* Allocates a chunk and adds it to the thread's Chunk list.
*/
private void allocate() {
int allocation = allocationSize();
Chunk chunk = (Chunk) allocator.allocate(allocation, null);
// This should always work just after allocation
chunk.validateFill();
chunks.add(chunk);
totalAllocation += chunk.getSize();
}
/**
* Frees a random chunk from the Chunk list.
*/
private void free() {
Chunk chunk = chunks.remove(random.nextInt(chunks.size()));
totalAllocation -= chunk.getSize();
/*
* Chunk is filled here but another thread may have already grabbed it so we
* cannot validate the fill.
*/
chunk.release();
}
/**
* Writes canned data to a random Chunk from the Chunk list.
*/
private void write() {
Chunk chunk = chunks.get(random.nextInt(chunks.size()));
chunk.writeBytes(0, WRITE_BYTES);
}
/**
* Randomly selects Chunk operations and executes them
* for a period of time. Collects any error thrown during execution.
*/
@Override
public void run() {
try {
for(long currentTime = System.currentTimeMillis();currentTime < endTime;currentTime = System.currentTimeMillis()) {
Operation op = (totalAllocation == 0 ? Operation.ALLOCATE : (totalAllocation >= MAX_WORKER_ALLOCATION_TOTAL_SIZE ? Operation.FREE : Operation.randomOperation()));
switch(op) {
case ALLOCATE:
allocate();
break;
case FREE:
free();
break;
case WRITE:
write();
break;
}
}
} catch (Throwable t) {
threadErrorList.add(t);
} finally {
latch.countDown();
}
}
}).start();
}
// Make sure each thread ended cleanly
assertTrue(latch.await(2, TimeUnit.MINUTES));
// Fail on the first error we find
if(!threadErrorList.isEmpty()) {
fail(threadErrorList.get(0).getMessage());
}
}
/**
* This tests that fill validation is working properly on newly created fragments after
* a compaction.
* @throws Exception
*/
@Test
public void testFillPatternAfterCompaction() throws Exception {
/*
* Stores our allocated memory.
*/
Chunk[] allocatedChunks = new Chunk[COMPACTION_CHUNKS];
/*
* Use up most of our memory
* Our memory looks like [ ][ ][ ]
*/
for(int i =0;i < allocatedChunks.length;++i) {
allocatedChunks[i] = (Chunk) this.allocator.allocate(COMPACTION_CHUNK_SIZE, null);
allocatedChunks[i].validateFill();
}
/*
* Release some of our allocated chunks.
*/
for(int i=0;i < 2;++i) {
allocatedChunks[i].release();
allocatedChunks[i].validateFill();
}
/*
* Now, allocate another chunk that is slightly larger than one of
* our initial chunks. This should force a compaction causing our
* memory to look like [ ][ ].
*/
Chunk slightlyLargerChunk = (Chunk) this.allocator.allocate(FORCE_COMPACTION_CHUNK_SIZE, null);
/*
* Make sure the compacted memory has the fill validation.
*/
slightlyLargerChunk.validateFill();
}
}