├── .gitignore ├── LICENSE ├── README.md ├── benchmark.plot ├── pom.xml ├── results.csv ├── results.png └── src ├── main ├── java │ └── com │ │ └── mitchellbosecke │ │ └── benchmark │ │ ├── BaseBenchmark.java │ │ ├── Freemarker.java │ │ ├── Handlebars.java │ │ ├── Mustache.java │ │ ├── Pebble.java │ │ ├── Rocker.java │ │ ├── Thymeleaf.java │ │ ├── Trimou.java │ │ ├── Velocity.java │ │ └── model │ │ └── Stock.java └── resources │ └── templates │ ├── stocks.freemarker.html │ ├── stocks.hbs.html │ ├── stocks.mustache.html │ ├── stocks.pebble.html │ ├── stocks.rocker.raw │ ├── stocks.thymeleaf.html │ ├── stocks.trimou.html │ └── stocks.velocity.html └── test ├── java └── com │ └── mitchellbosecke │ └── benchmark │ └── ExpectedOutputTest.java └── resources └── expected-output.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .classpath 3 | .settings 4 | target/ 5 | *.jar 6 | *.war 7 | *.ear 8 | .project 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Mitchell Bösecke 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | template-benchmark 2 | ================ 3 | 4 | JMH benchmark for popular Java template engines: 5 | 6 | * [Freemarker](http://freemarker.org/) 7 | * [Mustache](https://github.com/spullara/mustache.java) 8 | * [Pebble](http://www.mitchellbosecke.com/pebble) 9 | * [Rocker](https://github.com/fizzed/rocker) 10 | * [Thymeleaf](http://www.thymeleaf.org/) 11 | * [Trimou](http://trimou.org/) 12 | * [Velocity](http://velocity.apache.org/) 13 | 14 | Running the benchmark 15 | ====================== 16 | 17 | 1. Download the source code and build it (`mvn clean install`) 18 | 2. Run the entire benchmark suite with `java -jar target/benchmarks.jar` 19 | 3. (Optional) To run a single benchmark, such as Mustache, use `java -jar target/benchmarks.jar Mustache` 20 | 21 | Generating plot 22 | =============== 23 | 1. Run benchmark while exporting results to csv with `java -jar target/benchmarks.jar -rff results.csv -rf csv` 24 | 2. Use gnuplot to generate plot with `gnuplot benchmark.plot`. This will output `results.png`. 25 | 26 | Rules of Template Engine Configuration 27 | ====================================== 28 | It is imperative that each template engine is configured in way to reflect real-world usage as opposed to it's *optimal* configuration. Typically this means an out-of-the-box configuration. 29 | 30 | To strive for a common set of features across template engines, the following configurations are expected: 31 | * Disabling of HTML escaping 32 | * Template loaded from classpath prior to actual benchmark 33 | 34 | Interpreting the Results 35 | ======================== 36 | The benchmarks measure throughput, given in "ops/time". The time unit used is seconds. 37 | Generally, the score represents the number of templates rendered per second; the higher the score, the better. 38 | 39 | Example Results 40 | =============== 41 | 42 | ![Template Comparison](results.png) 43 | -------------------------------------------------------------------------------- /benchmark.plot: -------------------------------------------------------------------------------- 1 | # Labels 2 | set title 'Java Template Engine Performance Comparison' 3 | set ylabel 'Templates rendered per second' 4 | set xlabel 'Template Engine' 5 | set xtics nomirror rotate by -45 6 | 7 | # Ranges 8 | set autoscale 9 | 10 | # Input 11 | set datafile separator ',' 12 | 13 | # Output 14 | set terminal pngcairo enhanced font "Verdana,9" 15 | set output 'results.png' 16 | set grid 17 | set key off 18 | set boxwidth 0.8 relative 19 | 20 | # box style 21 | set style line 1 lc rgb '#5C91CD' lt 1 22 | set style fill solid 23 | 24 | # remove top and right borders 25 | set style line 2 lc rgb '#808080' lt 1 26 | set border 3 back ls 2 27 | set tics nomirror 28 | 29 | plot 'results.csv' every ::1 using 0:5:xticlabels(stringcolumn(1)[31:36]) with boxes ls 1,\ 30 | 'results.csv' every ::1 using 0:($5 + 1500):(sprintf("%d",$5)) with labels 31 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | Template Benchmark 5 | JMH Benchmark of Popular Template Engines 6 | 7 | 4.0.0 8 | com.mitchellbosecke 9 | template-benchmark 10 | 0.0.1-SNAPSHOT 11 | 12 | 13 | UTF-8 14 | 1.11.2 15 | benchmarks 16 | 17 | 2.2.0 18 | 0.9.1 19 | 2.3.23 20 | 1.7 21 | 2.1.4.RELEASE 22 | 4.12 23 | 1.8.2.Final 24 | 4.0.1 25 | 0.10.3 26 | 27 | 28 | 29 | 30 | BSD 3-Clause License 31 | http://opensource.org/licenses/BSD-3-Clause 32 | repo 33 | 34 | 35 | 36 | 37 | 38 | Mitchell Bösecke 39 | mitchellbosecke@gmail.com 40 | 41 | 42 | 43 | 44 | template-benchmark 45 | 46 | 47 | maven-compiler-plugin 48 | 3.1 49 | 50 | 1.7 51 | 1.7 52 | 53 | 54 | 55 | com.fizzed 56 | rocker-maven-plugin 57 | ${rocker.version} 58 | 59 | 60 | generate-rocker-templates 61 | generate-sources 62 | 63 | generate 64 | 65 | 66 | src/main/resources 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-shade-plugin 74 | 2.4.2 75 | 76 | 77 | package 78 | 79 | shade 80 | 81 | 82 | ${uberjar.name} 83 | 84 | 86 | org.openjdk.jmh.Main 87 | 88 | 89 | 90 | 91 | 92 | *:* 93 | 94 | META-INF/*.SF 95 | META-INF/*.DSA 96 | META-INF/*.RSA 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.openjdk.jmh 112 | jmh-core 113 | ${jmh.version} 114 | 115 | 116 | org.openjdk.jmh 117 | jmh-generator-annprocess 118 | ${jmh.version} 119 | provided 120 | 121 | 122 | 123 | 124 | com.mitchellbosecke 125 | pebble 126 | ${pebble.version} 127 | 128 | 129 | com.github.spullara.mustache.java 130 | compiler 131 | ${mustache.version} 132 | 133 | 134 | org.freemarker 135 | freemarker 136 | ${freemarker.version} 137 | 138 | 139 | org.apache.velocity 140 | velocity 141 | ${velocity.version} 142 | 143 | 144 | org.thymeleaf 145 | thymeleaf 146 | ${thymeleaf.version} 147 | 148 | 149 | org.trimou 150 | trimou-core 151 | ${trimou.version} 152 | 153 | 154 | com.github.jknack 155 | handlebars 156 | ${hbs.version} 157 | 158 | 159 | com.fizzed 160 | rocker-runtime 161 | ${rocker.version} 162 | 163 | 164 | 165 | junit 166 | junit 167 | test 168 | ${junit.version} 169 | 170 | 171 | 172 | 173 | 174 | scm:git:git://github.com/mbosecke/template-benchmark.git 175 | scm:git:git@github.com:mbosecke/template-benchmark.git 176 | http://github.com/mbosecke/template-benchmark 177 | 178 | 179 | -------------------------------------------------------------------------------- /results.csv: -------------------------------------------------------------------------------- 1 | "Benchmark","Mode","Threads","Samples","Score","Score Error (99.9%)","Unit" 2 | "com.mitchellbosecke.benchmark.Freemarker.benchmark","thrpt",1,50,15370.751504,496.200994,"ops/s" 3 | "com.mitchellbosecke.benchmark.Handlebars.benchmark","thrpt",1,50,17779.570147,552.411776,"ops/s" 4 | "com.mitchellbosecke.benchmark.Mustache.benchmark","thrpt",1,50,22164.070102,598.799830,"ops/s" 5 | "com.mitchellbosecke.benchmark.Pebble.benchmark","thrpt",1,50,32530.043940,849.278116,"ops/s" 6 | "com.mitchellbosecke.benchmark.Rocker.benchmark","thrpt",1,50,39739.408090,1152.244741,"ops/s" 7 | "com.mitchellbosecke.benchmark.Thymeleaf.benchmark","thrpt",1,50,1084.191177,33.698132,"ops/s" 8 | "com.mitchellbosecke.benchmark.Trimou.benchmark","thrpt",1,50,22787.431476,622.350790,"ops/s" 9 | "com.mitchellbosecke.benchmark.Velocity.benchmark","thrpt",1,50,20835.819309,526.312411,"ops/s" 10 | -------------------------------------------------------------------------------- /results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbosecke/template-benchmark/8b00e1fa38e097eeb108e205feb3a4f8a78bf6c2/results.png -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/BaseBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import org.openjdk.jmh.annotations.BenchmarkMode; 8 | import org.openjdk.jmh.annotations.Fork; 9 | import org.openjdk.jmh.annotations.Measurement; 10 | import org.openjdk.jmh.annotations.Mode; 11 | import org.openjdk.jmh.annotations.OutputTimeUnit; 12 | import org.openjdk.jmh.annotations.Scope; 13 | import org.openjdk.jmh.annotations.State; 14 | import org.openjdk.jmh.annotations.Warmup; 15 | 16 | import com.mitchellbosecke.benchmark.model.Stock; 17 | 18 | @Fork(5) 19 | @Warmup(iterations = 5) 20 | @Measurement(iterations = 10) 21 | @BenchmarkMode(Mode.Throughput) 22 | @OutputTimeUnit(TimeUnit.SECONDS) 23 | @State(Scope.Benchmark) 24 | public class BaseBenchmark { 25 | 26 | protected Map getContext() { 27 | Map context = new HashMap<>(); 28 | context.put("items", Stock.dummyItems()); 29 | return context; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Freemarker.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import java.io.IOException; 4 | import java.io.StringWriter; 5 | import java.io.Writer; 6 | import java.util.Map; 7 | 8 | import org.openjdk.jmh.annotations.Benchmark; 9 | import org.openjdk.jmh.annotations.Setup; 10 | 11 | import freemarker.cache.ClassTemplateLoader; 12 | import freemarker.template.Configuration; 13 | import freemarker.template.Template; 14 | import freemarker.template.TemplateException; 15 | 16 | public class Freemarker extends BaseBenchmark { 17 | 18 | private Map context; 19 | 20 | private Template template; 21 | 22 | @Setup 23 | public void setup() throws IOException { 24 | Configuration configuration = new Configuration(Configuration.VERSION_2_3_22); 25 | configuration.setTemplateLoader(new ClassTemplateLoader(getClass(), "/")); 26 | template = configuration.getTemplate("templates/stocks.freemarker.html"); 27 | this.context = getContext(); 28 | } 29 | 30 | @Benchmark 31 | public String benchmark() throws TemplateException, IOException { 32 | Writer writer = new StringWriter(); 33 | template.process(context, writer); 34 | return writer.toString(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Handlebars.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import java.io.IOException; 4 | 5 | import org.openjdk.jmh.annotations.Benchmark; 6 | import org.openjdk.jmh.annotations.Setup; 7 | 8 | import com.github.jknack.handlebars.Handlebars.SafeString; 9 | import com.github.jknack.handlebars.Helper; 10 | import com.github.jknack.handlebars.Options; 11 | import com.github.jknack.handlebars.Template; 12 | import com.github.jknack.handlebars.io.ClassPathTemplateLoader; 13 | import com.mitchellbosecke.benchmark.model.Stock; 14 | 15 | public class Handlebars extends BaseBenchmark { 16 | 17 | private Object context; 18 | 19 | private Template template; 20 | 21 | @Setup 22 | public void setup() throws IOException { 23 | template = new com.github.jknack.handlebars.Handlebars(new ClassPathTemplateLoader("/", ".html")) 24 | .registerHelper("minus", new Helper() { 25 | @Override 26 | public CharSequence apply(final Stock stock, final Options options) 27 | throws IOException { 28 | return stock.getChange() < 0 ? new SafeString("class=\"minus\"") : null; 29 | } 30 | }).compile("templates/stocks.hbs"); 31 | this.context = getContext(); 32 | } 33 | 34 | @Benchmark 35 | public String benchmark() throws IOException { 36 | return template.apply(context); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Mustache.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import java.io.IOException; 4 | import java.io.StringWriter; 5 | import java.io.Writer; 6 | import java.util.AbstractCollection; 7 | import java.util.Collection; 8 | import java.util.Iterator; 9 | import java.util.Map; 10 | 11 | import org.openjdk.jmh.annotations.Benchmark; 12 | import org.openjdk.jmh.annotations.Setup; 13 | 14 | import com.github.mustachejava.DefaultMustacheFactory; 15 | import com.github.mustachejava.MustacheException; 16 | import com.github.mustachejava.MustacheFactory; 17 | import com.mitchellbosecke.benchmark.model.Stock; 18 | 19 | public class Mustache extends BaseBenchmark { 20 | 21 | private com.github.mustachejava.Mustache template; 22 | 23 | @Setup 24 | public void setup() { 25 | MustacheFactory mustacheFactory = new DefaultMustacheFactory() { 26 | 27 | @Override 28 | public void encode(String value, Writer writer) { 29 | // Disable HTML escaping 30 | try { 31 | writer.write(value); 32 | } catch (IOException e) { 33 | throw new MustacheException(e); 34 | } 35 | } 36 | }; 37 | template = mustacheFactory.compile("templates/stocks.mustache.html"); 38 | } 39 | 40 | @SuppressWarnings("unchecked") 41 | @Benchmark 42 | public String benchmark() { 43 | 44 | Map data = getContext(); 45 | data.put("items", new StockCollection((Collection) data.get("items"))); 46 | 47 | Writer writer = new StringWriter(); 48 | template.execute(writer, data); 49 | return writer.toString(); 50 | } 51 | 52 | /** 53 | * This is a modified copy of 54 | * {@link com.github.mustachejava.util.DecoratedCollection} - we need the 55 | * first element at index 1. 56 | * 57 | * @param 58 | */ 59 | private class StockCollection extends AbstractCollection { 60 | 61 | private final Collection c; 62 | 63 | public StockCollection(Collection c) { 64 | this.c = c; 65 | } 66 | 67 | @Override 68 | public Iterator iterator() { 69 | final Iterator iterator = c.iterator(); 70 | return new Iterator() { 71 | 72 | int index = 1; 73 | 74 | @Override 75 | public boolean hasNext() { 76 | return iterator.hasNext(); 77 | } 78 | 79 | @Override 80 | public StockView next() { 81 | Stock next = iterator.next(); 82 | int current = index++; 83 | return new StockView(current, current == 1, !iterator.hasNext(), next); 84 | } 85 | 86 | @Override 87 | public void remove() { 88 | throw new UnsupportedOperationException(); 89 | } 90 | }; 91 | } 92 | 93 | @Override 94 | public int size() { 95 | return c.size(); 96 | } 97 | } 98 | 99 | class StockView { 100 | 101 | public final int index; 102 | 103 | public final boolean first; 104 | 105 | public final boolean last; 106 | 107 | public final Stock value; 108 | 109 | public final String negativeClass; 110 | 111 | public final String rowClass; 112 | 113 | public StockView(int index, boolean first, boolean last, Stock value) { 114 | this.index = index; 115 | this.first = first; 116 | this.last = last; 117 | this.value = value; 118 | this.negativeClass = value.getChange() > 0 ? "" : "class=\"minus\""; 119 | this.rowClass = index % 2 == 0 ? "even" : "odd"; 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Pebble.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import com.mitchellbosecke.pebble.PebbleEngine; 4 | import com.mitchellbosecke.pebble.error.PebbleException; 5 | import com.mitchellbosecke.pebble.template.PebbleTemplate; 6 | import org.openjdk.jmh.annotations.Benchmark; 7 | import org.openjdk.jmh.annotations.Setup; 8 | 9 | import java.io.IOException; 10 | import java.io.StringWriter; 11 | import java.util.Map; 12 | 13 | public class Pebble extends BaseBenchmark { 14 | 15 | private Map context; 16 | 17 | private PebbleTemplate template; 18 | 19 | @Setup 20 | public void setup() throws PebbleException { 21 | PebbleEngine engine = new PebbleEngine.Builder().autoEscaping(false).build(); 22 | template = engine.getTemplate("templates/stocks.pebble.html"); 23 | this.context = getContext(); 24 | } 25 | 26 | @Benchmark 27 | public String benchmark() throws PebbleException, IOException { 28 | StringWriter writer = new StringWriter(); 29 | template.evaluate(writer, context); 30 | return writer.toString(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Rocker.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import com.mitchellbosecke.benchmark.model.Stock; 4 | import freemarker.template.TemplateException; 5 | import org.openjdk.jmh.annotations.Benchmark; 6 | import org.openjdk.jmh.annotations.Setup; 7 | 8 | import java.io.IOException; 9 | import java.util.List; 10 | 11 | /** 12 | * Benchmark for Rocker template engine by Fizzed. 13 | * 14 | * https://github.com/fizzed/rocker 15 | * 16 | * @author joelauer 17 | */ 18 | public class Rocker extends BaseBenchmark { 19 | 20 | private List items; 21 | 22 | @Setup 23 | public void setup() throws IOException { 24 | // no config needed, replicate stocks from context 25 | this.items = Stock.dummyItems(); 26 | } 27 | 28 | @Benchmark 29 | public String benchmark() throws TemplateException, IOException { 30 | return templates.stocks 31 | .template(this.items) 32 | .render() 33 | .toString(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Thymeleaf.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import java.io.IOException; 4 | import java.io.StringWriter; 5 | import java.io.Writer; 6 | import java.util.Locale; 7 | 8 | import org.openjdk.jmh.annotations.Benchmark; 9 | import org.openjdk.jmh.annotations.Setup; 10 | import org.thymeleaf.TemplateEngine; 11 | import org.thymeleaf.context.Context; 12 | import org.thymeleaf.context.IContext; 13 | import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; 14 | 15 | import freemarker.template.TemplateException; 16 | 17 | public class Thymeleaf extends BaseBenchmark { 18 | 19 | private TemplateEngine engine; 20 | 21 | private IContext context; 22 | 23 | @Setup 24 | public void setup() throws IOException { 25 | engine = new TemplateEngine(); 26 | engine.setTemplateResolver(new ClassLoaderTemplateResolver()); 27 | context = new Context(Locale.getDefault(), getContext()); 28 | } 29 | 30 | @Benchmark 31 | public String benchmark() throws TemplateException, IOException { 32 | Writer writer = new StringWriter(); 33 | engine.process("templates/stocks.thymeleaf.html", context, writer); 34 | return writer.toString(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Trimou.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import java.util.Map; 4 | 5 | import org.openjdk.jmh.annotations.Benchmark; 6 | import org.openjdk.jmh.annotations.Setup; 7 | import org.trimou.engine.MustacheEngineBuilder; 8 | import org.trimou.engine.config.EngineConfigurationKey; 9 | import org.trimou.engine.locator.ClassPathTemplateLocator; 10 | import org.trimou.engine.resolver.CombinedIndexResolver; 11 | import org.trimou.handlebars.BasicValueHelper; 12 | import org.trimou.handlebars.HelpersBuilder; 13 | import org.trimou.handlebars.Options; 14 | 15 | public class Trimou extends BaseBenchmark { 16 | 17 | private Map context; 18 | 19 | private org.trimou.Mustache template; 20 | 21 | @Setup 22 | public void setup() { 23 | template = MustacheEngineBuilder.newBuilder() 24 | // Disable HTML escaping 25 | .setProperty(EngineConfigurationKey.SKIP_VALUE_ESCAPING, true) 26 | // Disable useless resolver 27 | .setProperty(CombinedIndexResolver.ENABLED_KEY, false) 28 | .addTemplateLocator(ClassPathTemplateLocator.builder(1).setRootPath("templates").setScanClasspath(false).setSuffix("trimou.html").build()) 29 | .registerHelpers(HelpersBuilder.extra().build()) 30 | // This is a single purpose helper 31 | // It's a pity we can't use JDK8 extension and SimpleHelpers util class 32 | .registerHelper("minusClass", new BasicValueHelper() { 33 | @Override 34 | public void execute(Options options) { 35 | Object value = options.getParameters().get(0); 36 | if (value instanceof Double && (Double) value < 0) { 37 | options.append(" class=\"minus\""); 38 | } 39 | // We don't handle any other number types 40 | } 41 | }).build().getMustache("stocks"); 42 | this.context = getContext(); 43 | } 44 | 45 | @Benchmark 46 | public String benchmark() { 47 | return template.render(context); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/Velocity.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import java.io.StringWriter; 4 | import java.io.Writer; 5 | import java.util.Properties; 6 | 7 | import org.apache.velocity.Template; 8 | import org.apache.velocity.VelocityContext; 9 | import org.apache.velocity.app.VelocityEngine; 10 | import org.openjdk.jmh.annotations.Benchmark; 11 | import org.openjdk.jmh.annotations.Setup; 12 | 13 | public class Velocity extends BaseBenchmark { 14 | 15 | private VelocityContext context; 16 | 17 | private Template template; 18 | 19 | @Setup 20 | public void setup() { 21 | Properties configuration = new Properties(); 22 | configuration.setProperty("resource.loader", "class"); 23 | configuration.setProperty("class.resource.loader.class", 24 | "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); 25 | 26 | VelocityEngine engine = new VelocityEngine(configuration); 27 | context = new VelocityContext(getContext()); 28 | template = engine.getTemplate("templates/stocks.velocity.html", "UTF-8"); 29 | } 30 | 31 | @Benchmark 32 | public String benchmark() { 33 | Writer writer = new StringWriter(); 34 | template.merge(context, writer); 35 | return writer.toString(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/mitchellbosecke/benchmark/model/Stock.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class Stock { 7 | 8 | private String name; 9 | 10 | private String name2; 11 | 12 | private String url; 13 | 14 | private String symbol; 15 | 16 | private double price; 17 | 18 | private double change; 19 | 20 | private double ratio; 21 | 22 | public Stock(String name, String name2, String url, String symbol, double price, double change, double ratio) { 23 | this.name = name; 24 | this.name2 = name2; 25 | this.url = url; 26 | this.symbol = symbol; 27 | this.price = price; 28 | this.change = change; 29 | this.ratio = ratio; 30 | } 31 | 32 | public String getName() { 33 | return this.name; 34 | } 35 | 36 | public String getName2() { 37 | return this.name2; 38 | } 39 | 40 | public String getUrl() { 41 | return this.url; 42 | } 43 | 44 | public String getSymbol() { 45 | return this.symbol; 46 | } 47 | 48 | public double getPrice() { 49 | return this.price; 50 | } 51 | 52 | public double getChange() { 53 | return this.change; 54 | } 55 | 56 | public double getRatio() { 57 | return this.ratio; 58 | } 59 | 60 | public static List dummyItems() { 61 | List items = new ArrayList(); 62 | items.add(new Stock("Adobe Systems", "Adobe Systems Inc.", "http://www.adobe.com", "ADBE", 39.26, 0.13, 0.33)); 63 | items.add(new Stock("Advanced Micro Devices", "Advanced Micro Devices Inc.", "http://www.amd.com", "AMD", 64 | 16.22, 0.17, 1.06)); 65 | items.add(new Stock("Amazon.com", "Amazon.com Inc", "http://www.amazon.com", "AMZN", 36.85, -0.23, -0.62)); 66 | items.add(new Stock("Apple", "Apple Inc.", "http://www.apple.com", "AAPL", 85.38, -0.87, -1.01)); 67 | items.add(new Stock("BEA Systems", "BEA Systems Inc.", "http://www.bea.com", "BEAS", 12.46, 0.09, 0.73)); 68 | items.add(new Stock("CA", "CA, Inc.", "http://www.ca.com", "CA", 24.66, 0.38, 1.57)); 69 | items.add(new Stock("Cisco Systems", "Cisco Systems Inc.", "http://www.cisco.com", "CSCO", 26.35, 0.13, 0.5)); 70 | items.add(new Stock("Dell", "Dell Corp.", "http://www.dell.com/", "DELL", 23.73, -0.42, -1.74)); 71 | items.add(new Stock("eBay", "eBay Inc.", "http://www.ebay.com", "EBAY", 31.65, -0.8, -2.47)); 72 | items.add(new Stock("Google", "Google Inc.", "http://www.google.com", "GOOG", 495.84, 7.75, 1.59)); 73 | items.add(new Stock("Hewlett-Packard", "Hewlett-Packard Co.", "http://www.hp.com", "HPQ", 41.69, -0.02, -0.05)); 74 | items.add(new Stock("IBM", "International Business Machines Corp.", "http://www.ibm.com", "IBM", 97.45, -0.06, 75 | -0.06)); 76 | items.add(new Stock("Intel", "Intel Corp.", "http://www.intel.com", "INTC", 20.53, -0.07, -0.34)); 77 | items.add(new Stock("Juniper Networks", "Juniper Networks, Inc", "http://www.juniper.net/", "JNPR", 18.96, 0.5, 78 | 2.71)); 79 | items.add(new Stock("Microsoft", "Microsoft Corp", "http://www.microsoft.com", "MSFT", 30.6, 0.15, 0.49)); 80 | items.add(new Stock("Oracle", "Oracle Corp.", "http://www.oracle.com", "ORCL", 17.15, 0.17, 1.1)); 81 | items.add(new Stock("SAP", "SAP AG", "http://www.sap.com", "SAP", 46.2, -0.16, -0.35)); 82 | items.add(new Stock("Seagate Technology", "Seagate Technology", "http://www.seagate.com/", "STX", 27.35, -0.36, 83 | -1.3)); 84 | items.add(new Stock("Sun Microsystems", "Sun Microsystems Inc.", "http://www.sun.com", "SUNW", 6.33, -0.01, 85 | -0.16)); 86 | items.add(new Stock("Yahoo", "Yahoo! Inc.", "http://www.yahoo.com", "YHOO", 28.04, -0.17, -0.6)); 87 | return items; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.freemarker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | <#list items as item> 57 | 58 | 59 | 60 | 61 | <#if (item.change < 0.0)> 62 | 63 | <#else> 64 | 65 | 66 | 67 | 68 | 69 |
#symbolnamepricechangeratio
${item_index + 1}${item.symbol}${item.name}${item.price}${item.change}${item.ratio}${item.change}${item.ratio}
70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.hbs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {{#each items base=1}} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {{/each}} 66 | 67 |
#symbolnamepricechangeratio
{{&@index}}{{&symbol}}{{&name}}{{&price}}{{&change}}{{&ratio}}
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.mustache.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {{#items}} 57 | 58 | 59 | 62 | 65 | 68 | {{value.change}} 69 | {{value.ratio}} 70 | 71 | {{/items}} 72 | 73 |
#symbolnamepricechangeratio
{{index}} 60 | {{value.symbol}} 61 | 63 | {{value.name}} 64 | 66 | {{value.price}} 67 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.pebble.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% for item in items %} 57 | 58 | 59 | 62 | 65 | 68 | 69 | {% if item.change < 0.0 %} 70 | 71 | 72 | {% else %} 73 | 74 | 75 | {% endif %} 76 | 77 | 78 | {% endfor %} 79 | 80 |
#symbolnamepricechangeratio
{{loop.index + 1}} 60 | {{item.symbol}} 61 | 63 | {{item.name}} 64 | 66 | {{ item.price }} 67 | {{item.change}}{{item.ratio}}{{item.change}}{{item.ratio}}
81 | 82 | 83 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.rocker.raw: -------------------------------------------------------------------------------- 1 | @import com.mitchellbosecke.benchmark.model.* 2 | @import java.util.List 3 | @args (List items) 4 | 5 | 6 | 7 | Stock Prices 8 | 9 | 10 | 11 | 12 | 13 | 14 | 40 | 41 | 42 | 43 | 44 | 45 |

Stock Prices

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | @for ((ForIterator i, Stock item) : items) { 60 | 61 | 62 | 63 | 64 | @if (item.getChange() < 0.0) { 65 | 66 | } else { 67 | 68 | } 69 | 70 | } 71 | 72 |
#symbolnamepricechangeratio
@Integer.toString(i.index()+1)@item.getSymbol()@item.getName()@item.getPrice()@item.getChange()@item.getRatio()@item.getChange()@item.getRatio()
73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.thymeleaf.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 60 | 62 | 63 | 64 | 66 | 68 | 69 | 70 |
#symbolnamepricechangeratio
71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.trimou.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {{#items}} 57 | 58 | 59 | 60 | 61 | 62 | {{change}} 63 | {{ratio}} 64 | 65 | {{/items}} 66 | 67 |
#symbolnamepricechangeratio
{{index}}{{symbol}}{{name}}{{price}}
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/resources/templates/stocks.velocity.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | #foreach($item in $items) 57 | #if($velocityCount % 2 == 0) #set($klass = "even") #else #set($klass = "odd") #end 58 | 59 | 60 | 63 | 66 | 69 | 70 | #if($item.change < 0.0) 71 | 72 | 73 | #else 74 | 75 | 76 | #end 77 | 78 | #end 79 | 80 |
#symbolnamepricechangeratio
${velocityCount} 61 | ${item.symbol} 62 | 64 | ${item.name} 65 | 67 | ${item.price} 68 | ${item.change}${item.ratio}${item.change}${item.ratio}
81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/test/java/com/mitchellbosecke/benchmark/ExpectedOutputTest.java: -------------------------------------------------------------------------------- 1 | package com.mitchellbosecke.benchmark; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.util.Locale; 9 | 10 | import org.junit.BeforeClass; 11 | import org.junit.Test; 12 | 13 | import com.mitchellbosecke.pebble.error.PebbleException; 14 | 15 | import freemarker.template.TemplateException; 16 | 17 | /** 18 | * 19 | * @author Martin Kouba 20 | */ 21 | public class ExpectedOutputTest { 22 | 23 | @BeforeClass 24 | public static void beforeClass() { 25 | Locale.setDefault(Locale.ENGLISH); 26 | } 27 | 28 | @Test 29 | public void testFreemarkerOutput() throws IOException, TemplateException { 30 | Freemarker freemarker = new Freemarker(); 31 | freemarker.setup(); 32 | assertOutput(freemarker.benchmark()); 33 | } 34 | 35 | @Test 36 | public void testRockerOutput() throws IOException, TemplateException { 37 | Rocker rocker = new Rocker(); 38 | rocker.setup(); 39 | assertOutput(rocker.benchmark()); 40 | } 41 | 42 | @Test 43 | public void testPebbleOutput() throws IOException, PebbleException { 44 | Pebble pebble = new Pebble(); 45 | pebble.setup(); 46 | assertOutput(pebble.benchmark()); 47 | } 48 | 49 | @Test 50 | public void testVelocityOutput() throws IOException { 51 | Velocity velocity = new Velocity(); 52 | velocity.setup(); 53 | assertOutput(velocity.benchmark()); 54 | } 55 | 56 | @Test 57 | public void testMustacheOutput() throws IOException { 58 | Mustache mustache = new Mustache(); 59 | mustache.setup(); 60 | assertOutput(mustache.benchmark()); 61 | } 62 | 63 | @Test 64 | public void testThymeleafOutput() throws IOException, TemplateException { 65 | Thymeleaf thymeleaf = new Thymeleaf(); 66 | thymeleaf.setup(); 67 | assertOutput(thymeleaf.benchmark()); 68 | } 69 | 70 | @Test 71 | public void testTrimouOutput() throws IOException { 72 | Trimou trimou = new Trimou(); 73 | trimou.setup(); 74 | assertOutput(trimou.benchmark()); 75 | } 76 | 77 | @Test 78 | public void testHbsOutput() throws IOException { 79 | Handlebars hbs = new Handlebars(); 80 | hbs.setup(); 81 | assertOutput(hbs.benchmark()); 82 | } 83 | 84 | private void assertOutput(final String output) throws IOException { 85 | assertEquals(readExpectedOutputResource(), output.replaceAll("\\s", "")); 86 | } 87 | 88 | private String readExpectedOutputResource() throws IOException { 89 | StringBuilder builder = new StringBuilder(); 90 | try (BufferedReader in = new BufferedReader(new InputStreamReader(ExpectedOutputTest.class.getResourceAsStream("/expected-output.html")))) { 91 | for (;;) { 92 | String line = in.readLine(); 93 | if (line == null) { 94 | break; 95 | } 96 | builder.append(line); 97 | } 98 | } 99 | // Remove all whitespaces 100 | return builder.toString().replaceAll("\\s", ""); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/resources/expected-output.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stock Prices 5 | 6 | 7 | 8 | 9 | 10 | 11 | 37 | 38 | 39 | 40 | 41 | 42 |

Stock Prices

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 |
#symbolnamepricechangeratio
1ADBEAdobe Systems39.260.130.33
2AMDAdvanced Micro Devices16.220.171.06
3AMZNAmazon.com36.85-0.23-0.62
4AAPLApple85.38-0.87-1.01
5BEASBEA Systems12.460.090.73
6CACA24.660.381.57
7CSCOCisco Systems26.350.130.5
8DELLDell23.73-0.42-1.74
9EBAYeBay31.65-0.8-2.47
10GOOGGoogle495.847.751.59
11HPQHewlett-Packard41.69-0.02-0.05
12IBMIBM97.45-0.06-0.06
13INTCIntel20.53-0.07-0.34
14JNPRJuniper Networks18.960.52.71
15MSFTMicrosoft30.60.150.49
16ORCLOracle17.150.171.1
17SAPSAP46.2-0.16-0.35
18STXSeagate Technology27.35-0.36-1.3
19SUNWSun Microsystems6.33-0.01-0.16
20YHOOYahoo28.04-0.17-0.6
238 | 239 | 240 | 241 | --------------------------------------------------------------------------------