Author: henrib Date: Wed Jan 6 19:46:18 2010 New Revision: 896643 URL: http://svn.apache.org/viewvc?rev=896643&view=rev Log: Refactored to to run multi-threaded; this verifies the expression cache and volatile AST node cache usage do not induce errors.
Modified: commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/CacheTest.java Modified: commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/CacheTest.java URL: http://svn.apache.org/viewvc/commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/CacheTest.java?rev=896643&r1=896642&r2=896643&view=diff ============================================================================== --- commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/CacheTest.java (original) +++ commons/proper/jexl/trunk/src/test/java/org/apache/commons/jexl2/CacheTest.java Wed Jan 6 19:46:18 2010 @@ -16,8 +16,13 @@ */ package org.apache.commons.jexl2; -import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; /** * Verifies cache & tryExecute @@ -33,7 +38,10 @@ jexl.setLenient(false); jexl.setSilent(false); } - private static final int LOOPS = 1024; + + // LOOPS & THREADS + private static final int LOOPS = 4096; + private static final int NTHREADS = 4; // A pseudo random mix of accessors private static final int[] MIX = { 0, 0, 3, 3, 4, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 1, 1, 1, 2, 2, 2, @@ -44,7 +52,12 @@ protected void tearDown() throws Exception { debuggerCheck(jexl); } - + + /** + * A set of classes that define different getter/setter methods for the same properties. + * The goal is to verify that the cached JexlPropertyGet / JexlPropertySet in the AST Nodes are indeed + * volatile and do not generate errors even when multiple threads concurently hammer them. + */ public static class Cached { public String compute(String arg) { if (arg == null) { @@ -237,6 +250,9 @@ } } + /** + * A helper class to pass arguments in tests (instances of getter/setter exercising classes). + */ static class TestCacheArguments { Cached0 c0 = new Cached0(); Cached1 c1 = new Cached1(); @@ -246,10 +262,17 @@ Object[] ca = { c0, c1, c2, c3, c4 }; - Object value = null; + Object[] value = null; } - void doAssign(TestCacheArguments x, int loops, boolean cache) throws Exception { + /** + * Run same test function in NTHREADS in parallel. + * @param ctask the task / test + * @param loops number of loops to perform + * @param cache whether jexl cache is used or not + * @throws Exception if anything goes wrong + */ + void runThreaded(Class<? extends Task> ctask, int loops, boolean cache) throws Exception { if (loops == 0) { loops = MIX.length; } @@ -258,251 +281,322 @@ } else { jexl.setCache(0); } - Map<String, Object> vars = new HashMap<String,Object>(); - JexlContext jc = new MapContext(vars); - Expression cacheGetValue = jexl.createExpression("cache.value"); - Expression cacheSetValue = jexl.createExpression("cache.value = value"); - Object result; + java.util.concurrent.ExecutorService execs = java.util.concurrent.Executors.newFixedThreadPool(NTHREADS); + List<Task> tasks = new ArrayList<Task>(NTHREADS); + for(int t = 0; t < NTHREADS; ++t) { + tasks.add(jexl.newInstance(ctask, loops)); + } + // let's not wait for more than a minute + List<Future<Integer>> futures = execs.invokeAll(tasks, 60, TimeUnit.SECONDS); + // check that all returned loops + for(Future<Integer> future : futures) { + assertEquals(Integer.valueOf(loops), future.get()); + } + } - for (int l = 0; l < loops; ++l) { - int mix = MIX[l % MIX.length]; + /** + * The base class for MT tests. + */ + public abstract static class Task implements Callable<Integer> { + final TestCacheArguments args = new TestCacheArguments(); + final int loops; + final Map<String, Object> vars = new HashMap<String, Object>(); + final JexlContext jc = new MapContext(vars); + + Task(int loops) { + this.loops = loops; + } + + public abstract Integer call() throws Exception; + + /** + * The actual test function; assigns and checks. + * <p>The expression will be evaluated against different classes in parallel. + * This verifies that neither the volatile cache in the AST nor the expression cache in the JEXL engine + * induce errors.</p> + * <p> + * Using it as a micro benchmark, it shows creating expression as the dominating cost; the expression + * cache takes care of this. + * By moving the expression creations out of the main loop, it also shows that the volatile cache speeds + * things up around 2x. + * </p> + * @param value the argument value to control + * @return the number of loops performed + */ + public Integer runAssign(Object value) { + args.value = new Object[]{value}; + Object result; + + Expression cacheGetValue = jexl.createExpression("cache.value"); + Expression cacheSetValue = jexl.createExpression("cache.value = value"); + for (int l = 0; l < loops; ++l) { + int px = (int) Thread.currentThread().getId(); + int mix = MIX[(l + px) % MIX.length]; + + vars.put("cache", args.ca[mix]); + vars.put("value", args.value[0]); + result = cacheSetValue.evaluate(jc); + if (args.value[0] == null) { + assertNull(cacheSetValue.toString(), result); + } else { + assertEquals(cacheSetValue.toString(), args.value[0], result); + } - vars.put("cache", x.ca[mix]); - vars.put("value", x.value); - result = cacheSetValue.evaluate(jc); - if (x.value == null) { - assertNull(cacheSetValue.toString(), result); - } else { - assertEquals(cacheSetValue.toString(), x.value, result); - } + result = cacheGetValue.evaluate(jc); + if (args.value[0] == null) { + assertEquals(cacheGetValue.toString(), "Cached" + mix + ":na", result); + } else { + assertEquals(cacheGetValue.toString(), "Cached" + mix + ":" + args.value[0], result); + } - result = cacheGetValue.evaluate(jc); - if (x.value == null) { - assertEquals(cacheGetValue.toString(), "Cached" + mix + ":na", result); - } else { - assertEquals(cacheGetValue.toString(), "Cached" + mix + ":" + x.value, result); } + return Integer.valueOf(loops); } } - public void testNullAssignNoCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - doAssign(args, LOOPS, false); + /** + * A task to check assignment. + */ + public static class AssignTask extends Task { + public AssignTask(int loops) { + super(loops); + } + public Integer call() throws Exception { + return runAssign("foo"); + } } - public void testNullAssignCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - doAssign(args, LOOPS, true); + /** + * A task to check null assignment. + */ + public static class AssignNullTask extends Task { + public AssignNullTask(int loops) { + super(loops); + } + public Integer call() throws Exception { + return runAssign(null); + } } - public void testAssignNoCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.value = "foo"; - doAssign(args, LOOPS, false); - } + /** + * A task to check boolean assignment. + */ + public static class AssignBooleanTask extends Task { + public AssignBooleanTask(int loops) { + super(loops); + } + public Integer call() throws Exception { + return runAssignBoolean(Boolean.TRUE); + } - public void testAssignCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.value = "foo"; - doAssign(args, LOOPS, true); + /** The actual test function. */ + private Integer runAssignBoolean(Boolean value) { + args.value = new Object[]{value}; + Expression cacheGetValue = jexl.createExpression("cache.flag"); + Expression cacheSetValue = jexl.createExpression("cache.flag = value"); + Object result; + + for (int l = 0; l < loops; ++l) { + int px = (int) Thread.currentThread().getId(); + int mix = MIX[(l + px) % MIX.length]; + + vars.put("cache", args.ca[mix]); + vars.put("value", args.value[0]); + result = cacheSetValue.evaluate(jc); + assertEquals(cacheSetValue.toString(), args.value[0], result); + + result = cacheGetValue.evaluate(jc); + assertEquals(cacheGetValue.toString(), args.value[0], result); + + } + + return Integer.valueOf(loops); + } } - void doAssignBoolean(TestCacheArguments x, int loops, boolean cache) throws Exception { - if (loops == 0) { - loops = MIX.length; + /** + * A task to check list assignment. + */ + public static class AssignListTask extends Task { + public AssignListTask(int loops) { + super(loops); } - if (cache) { - jexl.setCache(32); - } else { - jexl.setCache(0); + + public Integer call() throws Exception { + return runAssignList(); } - Map<String, Object> vars = new HashMap<String,Object>(); - JexlContext jc = new MapContext(vars); - Expression cacheGetValue = jexl.createExpression("cache.flag"); - Expression cacheSetValue = jexl.createExpression("cache.flag = value"); - Object result; + /** The actual test function. */ + private Integer runAssignList() { + args.value = new Object[]{"foo"}; + java.util.ArrayList<String> c1 = new java.util.ArrayList<String>(2); + c1.add("foo"); + c1.add("bar"); + args.ca = new Object[]{ + new String[]{"one", "two"}, + c1 + }; - for (int l = 0; l < loops; ++l) { - int mix = MIX[l % MIX.length]; + Expression cacheGetValue = jexl.createExpression("cache.0"); + Expression cacheSetValue = jexl.createExpression("cache[0] = value"); + Object result; - vars.put("cache", x.ca[mix]); - vars.put("value", x.value); - result = cacheSetValue.evaluate(jc); - assertEquals(cacheSetValue.toString(), x.value, result); + for (int l = 0; l < loops; ++l) { + int px = (int) Thread.currentThread().getId(); + int mix = MIX[(l + px) % MIX.length] % args.ca.length; - result = cacheGetValue.evaluate(jc); - assertEquals(cacheGetValue.toString(), x.value, result); + vars.put("cache", args.ca[mix]); + vars.put("value", args.value[0]); + result = cacheSetValue.evaluate(jc); + assertEquals(cacheSetValue.toString(), args.value[0], result); + + result = cacheGetValue.evaluate(jc); + assertEquals(cacheGetValue.toString(), args.value[0], result); + } + return Integer.valueOf(loops); } } - public void testAssignBooleanNoCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.value = Boolean.TRUE; - doAssignBoolean(args, LOOPS, false); - } - public void testAssignBooleanCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.value = Boolean.TRUE; - doAssignBoolean(args, LOOPS, true); + public void testNullAssignNoCache() throws Exception { + runThreaded(AssignNullTask.class, LOOPS, false); } - void doAssignList(TestCacheArguments x, int loops, boolean cache) throws Exception { - if (loops == 0) { - loops = MIX.length; - } - if (cache) { - jexl.setCache(32); - } else { - jexl.setCache(0); - } - Map<String, Object> vars = new HashMap<String,Object>(); - JexlContext jc = new MapContext(vars); - Expression cacheGetValue = jexl.createExpression("cache.0"); - Expression cacheSetValue = jexl.createExpression("cache[0] = value"); - Object result; + public void testNullAssignCache() throws Exception { + runThreaded(AssignNullTask.class, LOOPS, true); + } - for (int l = 0; l < loops; ++l) { - int mix = MIX[l % MIX.length] % x.ca.length; + public void testAssignNoCache() throws Exception { + runThreaded(AssignTask.class, LOOPS, false); + } - vars.put("cache", x.ca[mix]); - vars.put("value", x.value); - result = cacheSetValue.evaluate(jc); - assertEquals(cacheSetValue.toString(), x.value, result); + public void testAssignCache() throws Exception { + runThreaded(AssignTask.class, LOOPS, true); + } - result = cacheGetValue.evaluate(jc); - assertEquals(cacheGetValue.toString(), x.value, result); + public void testAssignBooleanNoCache() throws Exception { + runThreaded(AssignBooleanTask.class, LOOPS, false); + } - } + public void testAssignBooleanCache() throws Exception { + runThreaded(AssignBooleanTask.class, LOOPS, true); } public void testAssignListNoCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.value = "foo"; - java.util.ArrayList<String> c1 = new java.util.ArrayList<String>(2); - c1.add("foo"); - c1.add("bar"); - args.ca = new Object[]{ - new String[]{"one", "two"}, - c1 - }; - doAssignList(args, LOOPS, false); + runThreaded(AssignListTask.class, LOOPS, false); } public void testAssignListCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.value = "foo"; - java.util.ArrayList<String> c1 = new java.util.ArrayList<String>(2); - c1.add("foo"); - c1.add("bar"); - args.ca = new Object[]{ - new String[]{"one", "two"}, - c1 - }; - doAssignList(args, LOOPS, true); + runThreaded(AssignListTask.class, LOOPS, true); } - void doCompute(TestCacheArguments x, int loops, boolean cache) throws Exception { - if (loops == 0) { - loops = MIX.length; - } - if (cache) { - jexl.setCache(32); - } else { - jexl.setCache(0); - } - Map<String, Object> vars = new HashMap<String,Object>(); - JexlContext jc = new MapContext(vars); - jexl.setDebug(true); - Expression compute2 = jexl.createExpression("cache.compute(a0, a1)"); - Expression compute1 = jexl.createExpression("cache.compute(a0)"); - Expression compute1null = jexl.createExpression("cache.compute(a0)"); - Expression ambiguous = jexl.createExpression("cache.ambiguous(a0, a1)"); - jexl.setDebug(false); - Object result = null; - String expected = null; - for (int l = 0; l < loops; ++l) { - int mix = MIX[l % MIX.length] % x.ca.length; + /** + * A task to check method calls. + */ + public static class ComputeTask extends Task { + public ComputeTask(int loops) { + super(loops); + } + + public Integer call() throws Exception { + args.ca = new Object[]{args.c0, args.c1, args.c2}; + args.value = new Object[]{new Integer(2), "quux"}; + //jexl.setDebug(true); + Expression compute2 = jexl.createExpression("cache.compute(a0, a1)"); + Expression compute1 = jexl.createExpression("cache.compute(a0)"); + Expression compute1null = jexl.createExpression("cache.compute(a0)"); + Expression ambiguous = jexl.createExpression("cache.ambiguous(a0, a1)"); + //jexl.setDebug(false); + + Object result = null; + String expected = null; + for (int l = 0; l < loops; ++l) { + int mix = MIX[l % MIX.length] % args.ca.length; + Object value = args.value[l % args.value.length]; + + vars.put("cache", args.ca[mix]); + if (value instanceof String) { + vars.put("a0", "S0"); + vars.put("a1", "S1"); + expected = "Cached" + mix + "@s#S0,s#S1"; + } else if (value instanceof Integer) { + vars.put("a0", Integer.valueOf(7)); + vars.put("a1", Integer.valueOf(9)); + expected = "Cached" + mix + "@i#7,i#9"; + } else { + fail("unexpected value type"); + } + result = compute2.evaluate(jc); + assertEquals(compute2.toString(), expected, result); - vars.put("cache", x.ca[mix]); - if (x.value instanceof String) { - vars.put("a0", "S0"); - vars.put("a1", "S1"); - expected = "Cached" + mix + "@s#S0,s#S1"; - } else if (x.value instanceof Integer) { - vars.put("a0", Integer.valueOf(7)); - vars.put("a1", Integer.valueOf(9)); - expected = "Cached" + mix + "@i#7,i#9"; - } else { - fail("unexpected value type"); - } - result = compute2.evaluate(jc); - assertEquals(compute2.toString(), expected, result); + if (value instanceof Integer) { + try { + vars.put("a0", Short.valueOf((short) 17)); + vars.put("a1", Short.valueOf((short) 19)); + result = ambiguous.evaluate(jc); + fail("should have thrown an exception"); + } catch (JexlException xany) { + // throws due to ambiguous exception + } + } + + if (value instanceof String) { + vars.put("a0", "X0"); + expected = "Cached" + mix + "@s#X0"; + } else if (value instanceof Integer) { + vars.put("a0", Integer.valueOf(5)); + expected = "Cached" + mix + "@i#5"; + } else { + fail("unexpected value type"); + } + result = compute1.evaluate(jc); + assertEquals(compute1.toString(), expected, result); - if (x.value instanceof Integer) { try { - vars.put("a0", Short.valueOf((short) 17)); - vars.put("a1", Short.valueOf((short) 19)); - result = ambiguous.evaluate(jc); + vars.put("a0", null); + result = compute1null.evaluate(jc); fail("should have thrown an exception"); } catch (JexlException xany) { // throws due to ambiguous exception + String sany = xany.getMessage(); + String tname = getClass().getName(); + if (!sany.startsWith(tname)) { + fail("debug mode should carry caller information, " + + sany + ", " + + tname); + } } } - - if (x.value instanceof String) { - vars.put("a0", "X0"); - expected = "Cached" + mix + "@s#X0"; - } else if (x.value instanceof Integer) { - vars.put("a0", Integer.valueOf(5)); - expected = "Cached" + mix + "@i#5"; - } else { - fail("unexpected value type"); - } - result = compute1.evaluate(jc); - assertEquals(compute1.toString(), expected, result); - - try { - vars.put("a0", null); - jexl.setDebug(true); - result = compute1null.evaluate(jc); - fail("should have thrown an exception"); - } catch (JexlException xany) { - // throws due to ambiguous exception - String sany = xany.getMessage(); - String tname = getClass().getName(); - if (!sany.startsWith(tname)) { - fail("debug mode should carry caller information, " - + sany +", " - + tname); - } - } - finally { - jexl.setDebug(false); - } + return Integer.valueOf(loops); } } public void testComputeNoCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.ca = new Object[]{ - args.c0, args.c1, args.c2 - }; - args.value = new Integer(2); - doCompute(args, LOOPS, false); + try { + jexl.setDebug(true); + runThreaded(ComputeTask.class, LOOPS, false); + } finally { + jexl.setDebug(false); + } } public void testComputeCache() throws Exception { - TestCacheArguments args = new TestCacheArguments(); - args.ca = new Object[]{ - args.c0, args.c1, args.c2 - }; - args.value = new Integer(2); - doCompute(args, LOOPS, true); + try { + jexl.setDebug(true); + runThreaded(ComputeTask.class, LOOPS, true); + } finally { + jexl.setDebug(false); + } } + /** + * The remaining tests exercise the namespaced functions; not MT. + * @param x + * @param loops + * @param cache + * @throws Exception + */ void doCOMPUTE(TestCacheArguments x, int loops, boolean cache) throws Exception { if (loops == 0) { loops = MIX.length; @@ -512,7 +606,7 @@ } else { jexl.setCache(0); } - Map<String, Object> vars = new HashMap<String,Object>(); + Map<String, Object> vars = new HashMap<String, Object>(); JexlContext jc = new MapContext(vars); java.util.Map<String, Object> funcs = new java.util.HashMap<String, Object>(); jexl.setFunctions(funcs); @@ -522,13 +616,14 @@ String expected = null; for (int l = 0; l < loops; ++l) { int mix = MIX[l % MIX.length] % x.ca.length; + Object value = x.value[l % x.value.length]; funcs.put("cached", x.ca[mix]); - if (x.value instanceof String) { + if (value instanceof String) { vars.put("a0", "S0"); vars.put("a1", "S1"); expected = "cac...@s#s0,s#S1"; - } else if (x.value instanceof Integer) { + } else if (value instanceof Integer) { vars.put("a0", Integer.valueOf(7)); vars.put("a1", Integer.valueOf(9)); expected = "cac...@i#7,i#9"; @@ -538,10 +633,10 @@ result = compute2.evaluate(jc); assertEquals(compute2.toString(), expected, result); - if (x.value instanceof String) { + if (value instanceof String) { vars.put("a0", "X0"); expected = "cac...@s#x0"; - } else if (x.value instanceof Integer) { + } else if (value instanceof Integer) { vars.put("a0", Integer.valueOf(5)); expected = "cac...@i#5"; } else { @@ -555,19 +650,18 @@ public void testCOMPUTENoCache() throws Exception { TestCacheArguments args = new TestCacheArguments(); args.ca = new Object[]{ - Cached.class, Cached1.class, Cached2.class - }; - args.value = new Integer(2); + Cached.class, Cached1.class, Cached2.class + }; + args.value = new Object[]{new Integer(2), "quux"}; doCOMPUTE(args, LOOPS, false); } - + public void testCOMPUTECache() throws Exception { TestCacheArguments args = new TestCacheArguments(); args.ca = new Object[]{ - Cached.class, Cached1.class, Cached2.class - }; - args.value = new Integer(2); + Cached.class, Cached1.class, Cached2.class + }; + args.value = new Object[]{new Integer(2), "quux"}; doCOMPUTE(args, LOOPS, true); } - }