View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotNull;
21  
22  import java.text.DecimalFormat;
23  import java.text.NumberFormat;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Random;
27  import java.util.concurrent.ArrayBlockingQueue;
28  import java.util.concurrent.BlockingQueue;
29  import java.util.concurrent.Callable;
30  import java.util.concurrent.ExecutorService;
31  import java.util.concurrent.Executors;
32  import java.util.concurrent.Future;
33  
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  import org.junit.jupiter.api.Test;
37  
38  /**
39   * Testing JEXL caching performance.
40   * <p>
41   * The core idea is to create and evaluate scripts concurrently by a number (THREADS) of threads. The scripts source are number constants to keep the actual
42   * cost of evaluation to a minimum but we still want these to occur; the cost of creating the evaluation environment, etc is not negligible and there is no use
43   * in trying to cache/get scripts if its not to evaluate them. Each script is evaluated a number of times (HIT) in a tight loop to try and elicit good hit
44   * ratios but the number of potential scripts (SCRIPTS) is greater than the cache capacity (CACHED) to force eviction.
45   * </p>
46   * <p>
47   * The results vary a bit with parameters but the wall-clock times of the tests shows the worst case as 135% of best (the former being the default cache, the
48   * latter being the Google based cache). This indicates that the basic caching mechanism will likely not be a performance bottleneck in normal usage.
49   * </p>
50   */
51  public class CachePerformanceTest {
52      /**
53       * A task randomly chooses to run scripts (CACHED * HIT times). Tasks will be run
54       */
55      static class Task implements Callable<Integer> {
56          private final JexlEngine jexl;
57          private final BlockingQueue<?> queue;
58  
59          Task(final JexlEngine jexl, final BlockingQueue<?> queue) {
60              this.jexl = jexl;
61              this.queue = queue;
62          }
63  
64          @Override
65          public Integer call() {
66              int count = 0;
67              Object arg;
68              try {
69                  while ((arg = queue.take()) != Task.class) {
70                      final Random rnd = new Random((int) arg);
71                      for (int l = 0; l < LOOPS; ++l) {
72                          for (int c = 0; c < CACHED; ++c) {
73                              final int ctl = rnd.nextInt(SCRIPTS);
74                              for (int r = 0; r < HIT; ++r) {
75                                  final JexlScript script = jexl.createScript(Integer.toString(ctl));
76                                  final Object result = script.execute(null);
77                                  assert ((Number) result).intValue() == ctl;
78                                  count += 1;
79                              }
80                          }
81                      }
82                  }
83                  return count;
84              } catch (final InterruptedException e) {
85                  throw new RuntimeException(e);
86              }
87          }
88      }
89  
90      public static class Timer {
91          long begin;
92          long end;
93  
94          String elapse() {
95              final long delta = end - begin;
96              final NumberFormat fmt = new DecimalFormat("#.###");
97              return fmt.format(delta / 1000.d);
98          }
99  
100         void start() {
101             begin = System.currentTimeMillis();
102         }
103 
104         void stop() {
105             end = System.currentTimeMillis();
106         }
107     }
108 
109     /** Number of test loops. */
110     private static final int LOOPS = 10; // 0;
111     /** Number of different scripts. */
112     private static final int SCRIPTS = 800; // 0;
113     /** Cache capacity. */
114     private static final int CACHED = 500; // 0;
115     /** Number of times each script is evaluated. */
116     private static final int HIT = 5;
117 
118     /** Number of concurrent threads. */
119     private static final int THREADS = 8;
120 
121     /** Teh logger. */
122     Log LOGGER = LogFactory.getLog(getClass());
123 
124     /**
125      * Launches the tasks in parallel.
126      *
127      * @param jexl the jexl engine
128      * @throws Exception if something goes wrong
129      */
130     protected void runTest(final String name, final JexlEngine jexl) throws Exception {
131         final ExecutorService exec = Executors.newFixedThreadPool(THREADS);
132         final BlockingQueue<Object> queue = new ArrayBlockingQueue<>(THREADS);
133         final List<Future<Integer>> results = new ArrayList<>(THREADS);
134         // seed the cache
135         for (int i = 0; i < CACHED; ++i) {
136             final JexlScript script = jexl.createScript(Integer.toString(i));
137             assertNotNull(script);
138         }
139         // create a set of tasks ready to go
140         for (int t = 0; t < THREADS; ++t) {
141             results.add(exec.submit(new Task(jexl, queue)));
142         }
143         final Timer tt = new Timer();
144         tt.start();
145         // run each with its own sequence of random seeded by t
146         for (int t = 0; t < THREADS; ++t) {
147             queue.put(t);
148         }
149         // send the poison pill
150         for (int t = 0; t < THREADS; ++t) {
151             queue.put(Task.class);
152         }
153         int total = 0;
154         for (final Future<Integer> result : results) {
155             total += result.get();
156         }
157         exec.shutdown();
158         tt.stop();
159         assertEquals(total, LOOPS * CACHED * THREADS * HIT);
160         LOGGER.info(name + " : " + tt.elapse());
161     }
162 
163     @Test
164     public void testConcurrent() throws Exception {
165         final JexlBuilder builder = new JexlBuilder().cacheFactory(ConcurrentCache::new).cache(CACHED);
166         final JexlEngine jexl = builder.create();
167         runTest("testConcurrent", jexl);
168     }
169 
170     @Test
171     public void testSpread() throws Exception {
172         final JexlBuilder builder = new JexlBuilder().cacheFactory(SpreadCache::new).cache(CACHED);
173         final JexlEngine jexl = builder.create();
174         runTest("testSpread", jexl);
175     }
176 
177     @Test
178     public void testSynchronized() throws Exception {
179         final JexlBuilder builder = new JexlBuilder().cache(CACHED);
180         final JexlEngine jexl = builder.create();
181         runTest("testSynchronized", jexl);
182     }
183 }