diff --git a/.gitignore b/.gitignore index a90279a..70360f1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ target/ *.war *.ear .project +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 9911e36..9b2ae32 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ template-benchmark JMH benchmark for popular Java template engines: * [Freemarker](http://freemarker.org/) -* [Mustache](https://github.com/spullara/mustache.java) +* Mustache ([spullara](https://github.com/spullara/mustache.java) and [samskivert](https://github.com/samskivert/jmustachet) implementations) * [Pebble](http://www.mitchellbosecke.com/pebble) * [Rocker](https://github.com/fizzed/rocker) * [Thymeleaf](http://www.thymeleaf.org/) * [Trimou](http://trimou.org/) * [Velocity](http://velocity.apache.org/) +* [Groovy Template](http://groovy-lang.org/) Running the benchmark ====================== diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..75c6b91 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,52 @@ +jobs: +- job: Linux + timeoutInMinutes: 0 + pool: + vmImage: 'Ubuntu-16.04' + + steps: + - task: Maven@3 + inputs: + mavenPomFile: 'pom.xml' + mavenOptions: '-Xmx3072m' + javaHomeOption: 'JDKVersion' + jdkVersionOption: '1.8' + jdkArchitectureOption: 'x64' + publishJUnitResults: true + testResultsFiles: '**/surefire-reports/TEST-*.xml' + goals: 'package' + + - script: | + cat /proc/cpuinfo; lscpu + cat /proc/meminfo; free -m + displayName: 'Show hardware info' + + - script: | + less /proc/cpuinfo; lscpu + rm results.* + java -jar target/benchmarks.jar -rff results.csv -rf csv + cat results.csv + sed -i -E 's/com\.mitchellbosecke\.benchmark\.(\w+)\.benchmark/\1/g' results.csv + python generate-results-with-version.py + sed -i -E 's/Mustache_/Mustache /g' results.csv + cat results.csv + displayName: 'Exec benchmarks' + + + - script: sudo apt-get install -y gnuplot + displayName: 'Install gnuplot' + + - script: gnuplot benchmark.plot + displayName: 'Generate results.png' + + - task: CopyFiles@2 + inputs: + contents: | + **/*.jar + results.* + targetFolder: '$(build.artifactStagingDirectory)' + + - task: PublishBuildArtifacts@1 + inputs: + artifactName: 'results' + pathToPublish: '$(build.artifactStagingDirectory)' diff --git a/benchmark.plot b/benchmark.plot index b5e5243..a1cef11 100644 --- a/benchmark.plot +++ b/benchmark.plot @@ -13,7 +13,7 @@ set datafile separator ',' # Output set terminal pngcairo enhanced font "Verdana,9" set output 'results.png' -set grid +set grid lw 0.5 set key off set boxwidth 0.8 relative @@ -26,5 +26,6 @@ set style line 2 lc rgb '#808080' lt 1 set border 3 back ls 2 set tics nomirror -plot 'results.csv' every ::1 using 0:5:xticlabels(stringcolumn(1)[31:36]) with boxes ls 1,\ - 'results.csv' every ::1 using 0:($5 + 1500):(sprintf("%d",$5)) with labels +plot 'results.csv' every ::1 using 0:5:xticlabels(stringcolumn(1)) with boxes ls 1,\ + 'results.csv' every ::1 using 0:($5 + 1500):(sprintf("%d",$5)) with labels,\ + 'results.csv' every ::1 using 0:5:6 with yerrorbars linestyle -1 lc rgb 'black' lw 1 diff --git a/generate-results-with-version.py b/generate-results-with-version.py new file mode 100644 index 0000000..230c0b9 --- /dev/null +++ b/generate-results-with-version.py @@ -0,0 +1,35 @@ +from tempfile import NamedTemporaryFile +import shutil +import csv +from xml.etree import ElementTree +import subprocess + + +with open('pom.xml', 'rt') as f: + groups = ElementTree.parse(f) + +root = groups.getroot() +tree = root.findall('.//') +tree = tree[6].findall('.//') +versions = {} +for node in tree: + key = node.tag[node.tag.index("}") + 1:] + versions[key] = node.text + +filename = 'results.csv' +tempfile = NamedTemporaryFile(mode='w', delete=False) + +fields = ['Benchmark', 'Mode', 'Threads', 'Samples', 'Score', 'Score Error (99.9%)', 'Unit'] + +with open(filename, 'r') as csvfile:#, tempfile: + reader = csv.DictReader(csvfile, fieldnames=fields, delimiter=',') + writer = csv.DictWriter(tempfile, fieldnames=fields) + for row in reader: + if row['Benchmark'] != 'Benchmark': + key = "%s.version" % (row['Benchmark'].lower()) + version = versions[key] + row['Benchmark'] = row['Benchmark'] + " " + version + writer.writerow(row) + +shutil.move(tempfile.name, filename) +exit() diff --git a/pom.xml b/pom.xml index 74e3a7b..90e7bbe 100644 --- a/pom.xml +++ b/pom.xml @@ -11,17 +11,19 @@ UTF-8 - 1.7.1 + 1.21 benchmarks - 1.5.2 - 0.9.0 - 2.3.22 - 1.7 - 2.1.4.RELEASE + 3.0.6 + 1.14 + 0.9.5 + 2.3.28 + 2.0 + 3.0.11.RELEASE 4.11 - 1.8.1.Final - 0.9.0 + 2.5.0.Final + 1.2.0 + 2.5.4 @@ -46,8 +48,8 @@ maven-compiler-plugin 2.3.2 - 1.7 - 1.7 + 1.8 + 1.8 @@ -120,14 +122,19 @@ - com.mitchellbosecke + io.pebbletemplates pebble ${pebble.version} + + com.samskivert + jmustache + ${mustache_samskivert.version} + com.github.spullara.mustache.java compiler - ${mustache.version} + ${mustache_spullara.version} org.freemarker @@ -136,7 +143,7 @@ org.apache.velocity - velocity + velocity-engine-core ${velocity.version} @@ -154,6 +161,11 @@ rocker-runtime ${rocker.version} + + org.codehaus.groovy + groovy-templates + ${groovy.version} + junit diff --git a/results.csv b/results.csv index 99fd2d2..6568473 100644 --- a/results.csv +++ b/results.csv @@ -1,8 +1,10 @@ -"Benchmark","Mode","Threads","Samples","Score","Score Error (99.9%)","Unit" -"com.mitchellbosecke.benchmark.Freemarker.benchmark","thrpt",1,50.000000,16737.333340,184.040944,"ops/s" -"com.mitchellbosecke.benchmark.Mustache.benchmark","thrpt",1,50.000000,23393.115757,159.003408,"ops/s" -"com.mitchellbosecke.benchmark.Pebble.benchmark","thrpt",1,50.000000,35736.995844,248.860050,"ops/s" -"com.mitchellbosecke.benchmark.Rocker.benchmark","thrpt",1,50.000000,41123.645420,191.949947,"ops/s" -"com.mitchellbosecke.benchmark.Thymeleaf.benchmark","thrpt",1,50.000000,1519.092213,18.156292,"ops/s" -"com.mitchellbosecke.benchmark.Trimou.benchmark","thrpt",1,50.000000,19081.925071,112.440326,"ops/s" -"com.mitchellbosecke.benchmark.Velocity.benchmark","thrpt",1,50.000000,21553.362499,223.035824,"ops/s" +Benchmark,Mode,Threads,Samples,Score,Score Error (99.9%),Unit +Freemarker 2.3.28,thrpt,1,50,14375.008770,67.949534,ops/s +Groovy 2.5.4,thrpt,1,50,9916.720420,310.450084,ops/s +Mustache Samskivert 1.14,thrpt,1,50,9756.638916,201.174622,ops/s +Mustache Spullara 0.9.5,thrpt,1,50,16561.498801,51.512659,ops/s +Pebble 3.0.6,thrpt,1,50,19575.873030,372.340451,ops/s +Rocker 1.2.0,thrpt,1,50,29665.500272,326.030892,ops/s +Thymeleaf 3.0.11.RELEASE,thrpt,1,50,4547.712767,44.024793,ops/s +Trimou 2.5.0.Final,thrpt,1,50,16102.162324,207.259233,ops/s +Velocity 2.0,thrpt,1,50,16435.810422,124.612788,ops/s diff --git a/results.png b/results.png index c5b0329..e7a52c5 100644 Binary files a/results.png and b/results.png differ diff --git a/src/main/java/com/mitchellbosecke/benchmark/Groovy.java b/src/main/java/com/mitchellbosecke/benchmark/Groovy.java new file mode 100644 index 0000000..e7333aa --- /dev/null +++ b/src/main/java/com/mitchellbosecke/benchmark/Groovy.java @@ -0,0 +1,58 @@ +package com.mitchellbosecke.benchmark; + +import groovy.lang.Writable; +import groovy.text.Template; +import groovy.text.markup.MarkupTemplateEngine; +import groovy.text.markup.TemplateConfiguration; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Setup; + +import java.io.*; +import java.util.Map; + +public class Groovy extends BaseBenchmark { + + private Map context; + + private Template template; + + @Setup + public void setup() { + TemplateConfiguration config = new TemplateConfiguration(); + config.setAutoEscape(false); + config.setUseDoubleQuotes(true); + MarkupTemplateEngine engine = new MarkupTemplateEngine(config); + InputStream is = this.getClass().getClassLoader() + .getResourceAsStream("templates/stocks.groovy.tpl"); + Reader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(is, "UTF8")); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + + try { + template = engine.createTemplate(reader); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + + this.context = getContext(); + } + + @Benchmark + public String benchmark() { + Writer writer = new StringWriter(); + Writable output = template.make(context); + try { + output.writeTo(writer); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return writer.toString(); + } + +} diff --git a/src/main/java/com/mitchellbosecke/benchmark/Mustache_Samskivert.java b/src/main/java/com/mitchellbosecke/benchmark/Mustache_Samskivert.java new file mode 100644 index 0000000..2c210f2 --- /dev/null +++ b/src/main/java/com/mitchellbosecke/benchmark/Mustache_Samskivert.java @@ -0,0 +1,116 @@ +package com.mitchellbosecke.benchmark; + +import java.io.*; +import java.util.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +import com.samskivert.mustache.*; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Setup; + +import com.mitchellbosecke.benchmark.model.Stock; + +public class Mustache_Samskivert extends BaseBenchmark { + + private Template template; + + @Setup + public void setup() { + InputStream is = this.getClass().getClassLoader() + .getResourceAsStream("templates/stocks.mustache.html"); + Reader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(is, "UTF8")); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + + template = com.samskivert.mustache.Mustache.compiler().withEscaper(Escapers.NONE).compile(reader); + } + + @SuppressWarnings("unchecked") + @Benchmark + public String benchmark() { + + Map data = getContext(); + data.put("items", new StockCollection((Collection) data.get("items"))); + + Writer writer = new StringWriter(); + template.execute(data, writer); + return writer.toString(); + } + + /** + * This is a modified copy of + * {@link com.github.mustachejava.util.DecoratedCollection} - we need the + * first element at index 1. + * + * @param + */ + private class StockCollection extends AbstractCollection { + + private final Collection c; + + public StockCollection(Collection c) { + this.c = c; + } + + @Override + public Iterator iterator() { + final Iterator iterator = c.iterator(); + return new Iterator() { + + int index = 1; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public StockView next() { + Stock next = iterator.next(); + int current = index++; + return new StockView(current, current == 1, !iterator.hasNext(), next); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int size() { + return c.size(); + } + } + + class StockView { + + public final int index; + + public final boolean first; + + public final boolean last; + + public final Stock value; + + public final String negativeClass; + + public final String rowClass; + + public StockView(int index, boolean first, boolean last, Stock value) { + this.index = index; + this.first = first; + this.last = last; + this.value = value; + this.negativeClass = value.getChange() > 0 ? "" : "class=\"minus\""; + this.rowClass = index % 2 == 0 ? "even" : "odd"; + } + } + +} diff --git a/src/main/java/com/mitchellbosecke/benchmark/Mustache.java b/src/main/java/com/mitchellbosecke/benchmark/Mustache_Spullara.java similarity index 98% rename from src/main/java/com/mitchellbosecke/benchmark/Mustache.java rename to src/main/java/com/mitchellbosecke/benchmark/Mustache_Spullara.java index 69f8fe9..d3e87d1 100644 --- a/src/main/java/com/mitchellbosecke/benchmark/Mustache.java +++ b/src/main/java/com/mitchellbosecke/benchmark/Mustache_Spullara.java @@ -16,7 +16,7 @@ import com.github.mustachejava.MustacheFactory; import com.mitchellbosecke.benchmark.model.Stock; -public class Mustache extends BaseBenchmark { +public class Mustache_Spullara extends BaseBenchmark { private com.github.mustachejava.Mustache template; diff --git a/src/main/java/com/mitchellbosecke/benchmark/Pebble.java b/src/main/java/com/mitchellbosecke/benchmark/Pebble.java index cdf5d3a..f19ad06 100644 --- a/src/main/java/com/mitchellbosecke/benchmark/Pebble.java +++ b/src/main/java/com/mitchellbosecke/benchmark/Pebble.java @@ -20,8 +20,7 @@ public class Pebble extends BaseBenchmark { @Setup public void setup() throws PebbleException { - PebbleEngine engine = new PebbleEngine(); - engine.getExtension(EscaperExtension.class).setAutoEscaping(false); + PebbleEngine engine = new PebbleEngine.Builder().extension(new EscaperExtension()).autoEscaping(false).build(); template = engine.getTemplate("templates/stocks.pebble.html"); this.context = getContext(); } diff --git a/src/main/resources/templates/stocks.groovy.tpl b/src/main/resources/templates/stocks.groovy.tpl new file mode 100644 index 0000000..2484011 --- /dev/null +++ b/src/main/resources/templates/stocks.groovy.tpl @@ -0,0 +1,76 @@ +yieldUnescaped '' +html { + head { + title('Stock Prices') + meta('http-equiv':"Content-Type", content:"text/html; charset=UTF-8") + meta('http-equiv':"Content-Style-Type", 'content':"text/css") + meta('http-equiv':"Content-Script-Type", 'content':"text/javascript") + link('rel':"shortcut icon", 'href':"/images/favicon.ico") + link('rel':"stylesheet", 'type':"text/css", 'href':"/css/style.css", 'media':"all") + script('type':"text/javascript", 'src':"/js/util.js", "") + style('type':"text/css", """/**/""") + } + body { + h1('Stock Prices') + + table { + thead { + tr { + th('#') + th('symbol') + th('name') + th('price') + th('change') + th('ratio') + } + } + + tbody { + items.eachWithIndex {item, idx -> + tr (class: idx % 2 ? 'even' : 'odd'){ + td(idx + 1) + td { + a(item.symbol, href: '/stocks/' + item.symbol) + } + td { + a(item.name, href: item.url) + } + td { + strong(item.price) + } + if (item.change < 0) { + td(item.change, class: 'minus') + td(item.ratio, class: 'minus') + } else { + td(item.change) + td(item.ratio) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/templates/stocks.thymeleaf.html b/src/main/resources/templates/stocks.thymeleaf.html index a1667f6..ebf45a9 100755 --- a/src/main/resources/templates/stocks.thymeleaf.html +++ b/src/main/resources/templates/stocks.thymeleaf.html @@ -55,7 +55,7 @@

Stock Prices

+ th:class="${itemStat.odd}? 'odd' : 'even'"> @@ -70,4 +70,4 @@

Stock Prices

- \ No newline at end of file + diff --git a/src/main/resources/templates/stocks.velocity.html b/src/main/resources/templates/stocks.velocity.html index c3217ce..ab02f8a 100755 --- a/src/main/resources/templates/stocks.velocity.html +++ b/src/main/resources/templates/stocks.velocity.html @@ -54,9 +54,9 @@

Stock Prices

#foreach($item in $items) - #if($velocityCount % 2 == 0) #set($klass = "even") #else #set($klass = "odd") #end + #if($foreach.count % 2 == 0) #set($klass = "even") #else #set($klass = "odd") #end - ${velocityCount} + ${foreach.count} ${item.symbol} diff --git a/src/test/java/com/mitchellbosecke/benchmark/ExpectedOutputTest.java b/src/test/java/com/mitchellbosecke/benchmark/ExpectedOutputTest.java index 8320843..5feb49e 100644 --- a/src/test/java/com/mitchellbosecke/benchmark/ExpectedOutputTest.java +++ b/src/test/java/com/mitchellbosecke/benchmark/ExpectedOutputTest.java @@ -54,8 +54,15 @@ public void testVelocityOutput() throws IOException { } @Test - public void testMustacheOutput() throws IOException { - Mustache mustache = new Mustache(); + public void testMustache_SamskivertOutput() throws IOException { + Mustache_Samskivert mustache = new Mustache_Samskivert(); + mustache.setup(); + assertOutput(mustache.benchmark()); + } + + @Test + public void testMustache_SpullaraOutput() throws IOException { + Mustache_Spullara mustache = new Mustache_Spullara(); mustache.setup(); assertOutput(mustache.benchmark()); } @@ -74,6 +81,13 @@ public void testTrimouOutput() throws IOException { assertOutput(trimou.benchmark()); } + @Test + public void testGroovyOutput() throws IOException { + Groovy groovy = new Groovy(); + groovy.setup(); + assertOutput(groovy.benchmark()); + } + private void assertOutput(String output) throws IOException { assertEquals(readExpectedOutputResource(), output.replaceAll("\\s", "")); }