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    *      https://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  class CachePerformanceTest {
52  
53      /**
54       * A task randomly chooses to run scripts (CACHED * HIT times). Tasks will be run
55       */
56      static class Task implements Callable<Integer> {
57          private final JexlEngine jexl;
58          private final BlockingQueue<?> queue;
59  
60          Task(final JexlEngine jexl, final BlockingQueue<?> queue) {
61              this.jexl = jexl;
62              this.queue = queue;
63          }
64  
65          @Override
66          public Integer call() {
67              int count = 0;
68              Object arg;
69              try {
70                  while ((arg = queue.take()) != Task.class) {
71                      final Random rnd = new Random((int) arg);
72                      for (int l = 0; l < LOOPS; ++l) {
73                          for (int c = 0; c < CACHED; ++c) {
74                              final int ctl = rnd.nextInt(SCRIPTS);
75                              for (int r = 0; r < HIT; ++r) {
76                                  final JexlScript script = jexl.createScript(Integer.toString(ctl));
77                                  final Object result = script.execute(null);
78                                  assertEquals(((Number) result).intValue(), ctl);
79                                  count += 1;
80                              }
81                          }
82                      }
83                  }
84                  return count;
85              } catch (final InterruptedException e) {
86                  Thread.currentThread().interrupt();
87                  throw new RuntimeException(e);
88              }
89          }
90      }
91  
92      public static class Timer {
93          long begin;
94          long end;
95  
96          String elapse() {
97              final long delta = end - begin;
98              final NumberFormat fmt = new DecimalFormat("#.###");
99              return fmt.format(delta / 1000.d);
100         }
101 
102         void start() {
103             begin = System.currentTimeMillis();
104         }
105 
106         void stop() {
107             end = System.currentTimeMillis();
108         }
109     }
110 
111     /** Number of test loops. */
112     private static final int LOOPS = 10; // 0;
113 
114     /** Number of different scripts. */
115     private static final int SCRIPTS = 800; // 0;
116 
117     /** Cache capacity. */
118     private static final int CACHED = 500; // 0;
119 
120     /** Number of times each script is evaluated. */
121     private static final int HIT = 5;
122 
123     /** Number of concurrent threads. */
124     private static final int THREADS = 8;
125 
126     /** The logger. */
127     Log LOGGER = LogFactory.getLog(getClass());
128 
129     /**
130      * Launches the tasks in parallel.
131      *
132      * @param jexl the jexl engine
133      * @throws Exception if something goes wrong
134      */
135     protected void runTest(final String name, final JexlEngine jexl) throws Exception {
136         final ExecutorService exec = Executors.newFixedThreadPool(THREADS);
137         final BlockingQueue<Object> queue = new ArrayBlockingQueue<>(THREADS);
138         final List<Future<Integer>> results = new ArrayList<>(THREADS);
139         // seed the cache
140         for (int i = 0; i < CACHED; ++i) {
141             final JexlScript script = jexl.createScript(Integer.toString(i));
142             assertNotNull(script);
143         }
144         // create a set of tasks ready to go
145         for (int t = 0; t < THREADS; ++t) {
146             results.add(exec.submit(new Task(jexl, queue)));
147         }
148         final Timer tt = new Timer();
149         tt.start();
150         // run each with its own sequence of random seeded by t
151         for (int t = 0; t < THREADS; ++t) {
152             queue.put(t);
153         }
154         // send the poison pill
155         for (int t = 0; t < THREADS; ++t) {
156             queue.put(Task.class);
157         }
158         int total = 0;
159         for (final Future<Integer> result : results) {
160             total += result.get();
161         }
162         exec.shutdown();
163         tt.stop();
164         assertEquals(total, LOOPS * CACHED * THREADS * HIT);
165         LOGGER.info(name + " : " + tt.elapse());
166     }
167 
168     @Test
169     void testConcurrent() throws Exception {
170         final JexlBuilder builder = new JexlBuilder().cacheFactory(ConcurrentCache::new).cache(CACHED);
171         final JexlEngine jexl = builder.create();
172         runTest("testConcurrent", jexl);
173     }
174 
175     @Test
176     void testSpread() throws Exception {
177         final JexlBuilder builder = new JexlBuilder().cacheFactory(SpreadCache::new).cache(CACHED);
178         final JexlEngine jexl = builder.create();
179         runTest("testSpread", jexl);
180     }
181 
182     @Test
183     void testSynchronized() throws Exception {
184         final JexlBuilder builder = new JexlBuilder().cache(CACHED);
185         final JexlEngine jexl = builder.create();
186         runTest("testSynchronized", jexl);
187     }
188 }