/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Jan 11, 2010 */ package com.bigdata.cache; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.Callable; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import junit.framework.TestCase2; import com.bigdata.testutil.ExperimentDriver; import com.bigdata.testutil.XorShift; import com.bigdata.testutil.ExperimentDriver.IComparisonTest; import com.bigdata.testutil.ExperimentDriver.Result; import com.bigdata.util.DaemonThreadFactory; import com.bigdata.util.NV; /** * Test suite for {@link HardReferenceQueueWithBatchingUpdates}. The class under * test provides a thread-local buffer for updates * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ * * @todo There is a lot more that could be tested here. */ public class TestHardReferenceQueueWithBatchingUpdates extends TestCase2 implements IComparisonTest { /** * */ public TestHardReferenceQueueWithBatchingUpdates() { } /** * @param name */ public TestHardReferenceQueueWithBatchingUpdates(String name) { super(name); } /** * Basic test verifies that eviction is deferred until a batch is ready. */ public void test01() { final HardReferenceQueueEvictionListener<String> listener = null; // capacity of the shared queue. final int capacity = 27; // #of elements to test in each thread-local queue on add. final int threadLocalNSCan = 4; // capacity of the thread-local queue. final int threadLocalQueueCapacity = 4; // may be zero to disable tryLock() (makes test more deterministic). final int threadLocalTryLockSize = 0; final String ref0 = "0"; final String ref1 = "1"; final String ref2 = "2"; final String ref3 = "3"; final String ref4 = "4"; final String ref5 = "5"; final IHardReferenceQueue<String> q = new HardReferenceQueueWithBatchingUpdates<String>( new HardReferenceQueue<String>(listener, capacity, 0/* nscan */), // listener, capacity, threadLocalNSCan, threadLocalQueueCapacity, threadLocalTryLockSize, null// batched updates listener. ); // add ref, but not batched through. q.add(ref0); assertEquals(0, q.size()); // same ref, so eliminated by scan. q.add(ref0); assertEquals(0, q.size()); // 2nd ref. q.add(ref1); assertEquals(0, q.size()); // 3rd ref. q.add(ref2); assertEquals(0, q.size()); // 4th ref. causes batch eviction to the shared buffer before adding // the new element. q.add(ref3); assertEquals(3, q.size()); } /** * A default configuration of a parameterized stress test. * * @throws ExecutionException * @throws TimeoutException * @throws BrokenBarrierException * @throws InterruptedException */ public void test_stress() throws InterruptedException, BrokenBarrierException, TimeoutException, ExecutionException { final XorShift r = new XorShift(); // final int spinMax = 0;//50000; final int ndistinct = 10000; // final long waitMillisMax = 20; // pre-populate array of reference objects since Integer.valueOf() blocks. final Integer[] vals = new Integer[ndistinct]; for (int i = 0; i < ndistinct; i++) { vals[i] = Integer.valueOf(i); } doStressTest(// 10L,// timeout TimeUnit.SECONDS,// unit false, // threadLocalBuffers (vs striped locks) 16, // concurrencyLevel (typically GTE threadPoolSize) 1, // threadPoolSize (#of test driver threads) 8000, // queue capacity 0, // threadLocalNScan 64,//32,//64, // threadLocalQueueCapacity 32,//16,//32, // threadLocalTryLockSize new Callable<Object>() { public Object call() throws Exception { // final int nspin = spinMax == 0 ? 0 : Math.abs(r.next() // % spinMax); // for (int i = 0; i < nspin; i++) { // // spin // } if (r.next() % 10 == 1) { Thread.yield(); // final long waitMillis = Math.abs(r.next() // % waitMillisMax); // if (waitMillis > 0) // Thread.sleep(waitMillis); } return vals[Math.abs(r.next() % ndistinct)]; } }); } /** * A parameterized stress test in which a pool of threads drives adds to the * queue. The adds will go onto the thread-local queues first and should * then be batched to the backing shared queue. The object references are * drawn from a pool of references. This tests thread safety and may also be * used to measure the impact of different parameters on throughput, * including the scan of the thread-local queue for matching references, the * capacity of the thread-local queue, the size of the thread-local queue at * which an attempt is made to barge in on the lock, and the effect of the * #of concurrent threads. A workload function may also be configured, in * which case the latency of the thread's primary operation and the effects * of blocking waits (which are similar to waiting on IO) may be explored. * * @param timeout * The duration of the stress test. * @param unit * The unit in which the test duration is measured. * @param threadPoolSize * The size of the thread pool. * @param capacity * The capacity of the shared queue (does not include the * capacity of the thread-local queues). * @param threadLocalNScan * The #of references to scan on the thread-local queue. * @param threadLocalQueueCapacity * The capacity of the thread-local queues in which the updates * are gathered before they are batched into the shared queue. * This must be at least * @param threadLocalTryLockSize * Once the thread-local queue is this full an attempt will be * made to barge in on the lock and batch the updates to the * shared queue. This feature may be disabled by passing ZERO * (0). * * @return touches per unit. * * @throws BrokenBarrierException * @throws InterruptedException * @throws TimeoutException * @throws ExecutionException */ public long doStressTest(// final long timeout, final TimeUnit unit,// final boolean threadLocalBuffers,// final int concurrencyLevel,// final int threadPoolSize,// final int capacity,// final int threadLocalNScan,// final int threadLocalQueueCapacity,// final int threadLocalTryLockSize, // final Callable<?> worker// ) throws InterruptedException, BrokenBarrierException, TimeoutException, ExecutionException { final HardReferenceQueueWithBatchingUpdates<Object> queue = new HardReferenceQueueWithBatchingUpdates<Object>( threadLocalBuffers,// concurrencyLevel,// new HardReferenceQueue<Object>(// null,// listener capacity,// 0// nscan ), // null/* listener */, capacity, threadLocalNScan, // threadLocalQueueCapacity,// threadLocalTryLockSize, // null// batched updates listener ); final ExecutorService service = Executors.newFixedThreadPool( threadPoolSize, new DaemonThreadFactory(getName())); final AtomicLong ntouch = new AtomicLong(0L); final AtomicLong nadd = new AtomicLong(0L); final long elapsed; try { final AtomicBoolean done = new AtomicBoolean(false); final CyclicBarrier barrier = new CyclicBarrier(threadPoolSize + 1); final List<Future<Object>> futures = new ArrayList<Future<Object>>( threadPoolSize); for (int i = 0; i < threadPoolSize; i++) { final Callable<Object> task = new Callable<Object>() { long localTouchCount = 0L; long localAddCount = 0L; public Object call() throws Exception { barrier.await(); try { while (!done.get()) { final Object ref = worker.call(); if (queue.add(ref)) localAddCount++; localTouchCount++; } } catch (Throwable t) { // Note failures eagerly. System.err.println("Task failed: " + t + ", thread=" + Thread.currentThread()); throw new RuntimeException(t); } finally { // update the global counters. ntouch.addAndGet(localTouchCount); nadd.addAndGet(localAddCount); } // done. return null; } }; futures.add(service.submit(task)); } // wait for the barrier to break and then for the test to end. { assert !barrier.isBroken(); barrier.await(); // time when the test starts. final long begin = System.nanoTime(); // time remaining in the test. final long nanos = unit.toNanos(timeout); long remaining = nanos; System.err.println("Running..."); while (remaining >= 0) { Thread.sleep(TimeUnit.NANOSECONDS.toMillis(remaining), (int) Math.min(999999, remaining)); remaining = nanos - (System.nanoTime() - begin); } final long end = System.nanoTime(); elapsed = end - begin; // tell worker tasks to halt. done.set(true); } // shutdown the executor service. { // normal shutdown. service.shutdown(); if (!service.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS)) { fail("Service did not terminate"); } // verify no errors in the worker tasks. int nerrors = 0; for (Future<Object> f : futures) { try { f.get(); } catch (ExecutionException t) { nerrors++; t.printStackTrace(System.err); } } if (nerrors > 0) fail("There were " + nerrors + " errors."); } { final NumberFormat f = DecimalFormat.getNumberInstance(); f.setGroupingUsed(true); final long elapsedUnits = unit.convert(elapsed, TimeUnit.NANOSECONDS); final long touchesPerUnit = ntouch.get() / elapsedUnits; final long addsPerUnit = nadd.get() / elapsedUnits; System.err.println("timeout = " + f.format(timeout) + " " + unit.toString()); System.err.println("elapsed = " + f.format(elapsedUnits) + " " + unit.toString()); System.err.println("ntouches = " + f.format(ntouch) + ", per " + unit + "=" + f.format(touchesPerUnit)); System.err.println("nadded = " + f.format(nadd) + ", per " + unit + "=" + f.format(addsPerUnit)); System.err.println("ndups = " + f.format(ntouch.get() - nadd.get())); return touchesPerUnit; } } finally { // terminate the thread pool on error. service.shutdownNow(); } } @Override public void setUpComparisonTest(Properties properties) throws Exception { // queue = xxx; } @Override public void tearDownComparisonTest() throws Exception { // queue = null; } // private IHardReferenceQueue<Object> queue; /** * Additional properties understood by this test. */ //Removed in BLZG-1497 to remove package dependency. Keep if CI is clean. //public static interface TestOptions extends ConcurrencyManager.Options { public static interface TestOptions { /** * The timeout for the test. */ public static final String TIMEOUT = "timeout"; public static final String UNITS = "units"; /** * The #of concurrent threads to run. */ public static final String NTHREADS = "nthreads"; /** * The capacity of the shared queue. */ public static final String CAPACITY = "capacity"; /** * The #of elements to scan on the thread local queue in order to * identify duplicates. */ public static final String THREAD_LOCAL_NSCAN = "threadLocalNScan"; /** * The capacity of the thread local queue. */ public static final String THREAD_LOCAL_CAPACITY = "threadLocalCapacity"; /** * The size of the thread local queue at which an attempt will be made * to barge in on the lock and batch the updates to the shared queue. */ public static final String THREAD_LOCAL_TRY_LOCK_SIZE = "threadLocalTryLockSize"; } /** * Setup and run a test. * * @param properties * There are no "optional" properties - you must make sure that * each property has a defined value. */ public Result doComparisonTest(Properties properties) throws Exception { final long timeout = Long.parseLong(properties .getProperty(TestOptions.TIMEOUT)); final TimeUnit units = TimeUnit.valueOf(properties .getProperty(TestOptions.UNITS)); final boolean threadLocalBuffers = true;// TODO param final int concurrencyLevel = 16; // TODO param final int threadPoolSize = Integer.parseInt(properties .getProperty(TestOptions.NTHREADS)); final int capacity = Integer.parseInt(properties .getProperty(TestOptions.CAPACITY)); final int threadLocalNScan= Integer.parseInt(properties .getProperty(TestOptions.THREAD_LOCAL_NSCAN)); final int threadLocalCapacity = Integer.parseInt(properties .getProperty(TestOptions.THREAD_LOCAL_CAPACITY)); final int threadLocalTryLockSize = Integer.parseInt(properties .getProperty(TestOptions.THREAD_LOCAL_TRY_LOCK_SIZE)); final Result result = new Result(); /* * Setup the worker. */ final XorShift r = new XorShift(); final int ndistinct = 10000; // pre-populate array of reference objects since Integer.valueOf() // blocks. final Integer[] vals = new Integer[ndistinct]; for (int i = 0; i < ndistinct; i++) { vals[i] = Integer.valueOf(i); } final long touchesPerUnit = doStressTest(timeout, units, // threadLocalBuffers,// concurrencyLevel,// threadPoolSize, // capacity,// threadLocalNScan, threadLocalCapacity, threadLocalTryLockSize, new Callable<Object>() { public Object call() throws Exception { // final int nspin = spinMax == 0 ? 0 : Math.abs(r.next() // % spinMax); // for (int i = 0; i < nspin; i++) { // // spin // } if (r.next() % 100 == 1) { Thread.yield(); // final long waitMillis = Math.abs(r.next() // % waitMillisMax); // if (waitMillis > 0) // Thread.sleep(waitMillis); } return vals[Math.abs(r.next() % ndistinct)]; }}); result.put("touches/unit", "" + touchesPerUnit); return result; } /** * Experiment generation utility class. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> * @version $Id$ */ public static class GenerateExperiment extends ExperimentDriver { /** * Generates an XML file that can be run by {@link ExperimentDriver}. * * @param args * ignored. */ public static void main(final String[] args) throws Exception { // this is the test to be run. final String className = TestHardReferenceQueueWithBatchingUpdates.class.getName(); /* * Set defaults for each condition. */ final Map<String,String> defaultProperties = new HashMap<String,String>(); defaultProperties.put(TestOptions.TIMEOUT, "10"); defaultProperties.put(TestOptions.UNITS, TimeUnit.SECONDS.name()); defaultProperties.put(TestOptions.CAPACITY,"8000"); defaultProperties.put(TestOptions.THREAD_LOCAL_NSCAN,"0"); /* * Build up the conditions. */ List<Condition>conditions = new ArrayList<Condition>(); conditions.add(new Condition(defaultProperties)); conditions = apply(conditions, new NV[] { new NV(TestOptions.NTHREADS, "1"), new NV(TestOptions.NTHREADS, "2"), new NV(TestOptions.NTHREADS, "4"), new NV(TestOptions.NTHREADS, "8"), new NV(TestOptions.NTHREADS, "16"), }); conditions = apply( conditions, new NV[][] { // new NV[] { new NV(TestOptions.THREAD_LOCAL_CAPACITY, "16"), new NV( TestOptions.THREAD_LOCAL_TRY_LOCK_SIZE, "8"), }, // new NV[] { new NV(TestOptions.THREAD_LOCAL_CAPACITY, "32"), new NV( TestOptions.THREAD_LOCAL_TRY_LOCK_SIZE, "16"), }, // new NV[] { new NV(TestOptions.THREAD_LOCAL_CAPACITY, "64"), new NV( TestOptions.THREAD_LOCAL_TRY_LOCK_SIZE, "32"), }, // new NV[] { new NV(TestOptions.THREAD_LOCAL_CAPACITY, "128"), new NV( TestOptions.THREAD_LOCAL_TRY_LOCK_SIZE, "64"), }, // // }); final Experiment exp = new Experiment(className, defaultProperties, conditions); // copy the output into a file and then you can run it later. System.err.println(exp.toXML()); } } }