Browse Source

Merge pull request #1862 from pmlopes/frameworks/vertx-web

Frameworks/vertx web
Mike Smith 9 years ago
parent
commit
1e95e3a078

+ 1 - 0
.travis.yml

@@ -96,6 +96,7 @@ env:
     - "TESTDIR=Java/undertow"
     - "TESTDIR=Java/undertow"
     - "TESTDIR=Java/undertow-edge"
     - "TESTDIR=Java/undertow-edge"
     - "TESTDIR=Java/vertx"
     - "TESTDIR=Java/vertx"
+    - "TESTDIR=Java/vertx-web"
     - "TESTDIR=Java/wicket"
     - "TESTDIR=Java/wicket"
     - "TESTDIR=Java/wildfly-ee7"
     - "TESTDIR=Java/wildfly-ee7"
     - "TESTDIR=JavaScript/express"
     - "TESTDIR=JavaScript/express"

+ 4 - 0
frameworks/Java/vertx-web/.gitignore

@@ -0,0 +1,4 @@
+.idea
+.vertx
+target
+*.iml

+ 63 - 0
frameworks/Java/vertx-web/Readme.md

@@ -0,0 +1,63 @@
+# Vert.x Web Benchmarking Test
+
+This is the Vert.x Web portion of a [benchmarking test suite](../) comparing a variety of web development platforms.
+
+## Test URLs
+
+### Plain Text Test
+
+http://localhost:8080/plaintext
+
+### JSON Encoding Test
+
+http://localhost:8080/json
+
+### Data-Store/Database Mapping Test
+
+http://localhost:8080/mongo/db
+http://localhost:8080/mongo/queries?queries=5
+
+http://localhost:8080/jdbc/db
+http://localhost:8080/jdbc/queries?queries=5
+
+### Data-Store/Database Update Test
+
+http://localhost:8080/mongo/update?queries=5
+
+http://localhost:8080/jdbc/update?queries=5
+
+### Template rendering Test
+
+http://localhost:8080/mongo/fortunes
+
+http://localhost:8080/jdbc/fortunes
+
+## Generating Load
+It's best to generate load from a completely separate machine from the server if you can, to avoid resource contention
+during the test.
+
+We use the [wrk](https://github.com/wg/wrk) load generation tool to generate the load for our benchmark runs. It's the
+best tool we've found for the job and supports HTTP pipelining (used by the plaintext scenario) via its scripting
+interface. Wrk will only run from a Linux machine however, so if you must use Windows, try using
+[ab](https://httpd.apache.org/docs/2.2/programs/ab.html) (Apache Bench).
+
+You'll need to clone the [wrk repo](https://github.com/wg/wrk) on your load generation machine and follow
+[their instructions to build it](https://github.com/wg/wrk/wiki/Installing-Wrk-on-Linux).
+
+Here's a sample wrk command to generate load for the JSON scenario. This run is using 256 connections across 32 client
+threads for a duration of 10 seconds.
+
+```
+wrk -c 256 -t 32 -d 10 http://127.0.0.1:8080/json
+```
+
+To generate pipelined load for the plaintext scenario, use the following command, assuming your CWD is the root of this
+repo and wrk is on your path. The final argument after the `--` is the desired pipeline depth. We always run the
+plaintext scenario at a pipeline depth of 16, [just like the Techempower Benchmarks](https://github.com/TechEmpower/FrameworkBenchmarks/blob/6594d32db618c6ca65e0106c5adf2671f7b63654/toolset/benchmark/framework_test.py#L640).
+
+```
+wrk -c 256 -t 32 -d 10 -s ./scripts/pipeline.lua http://127.0.0.1:8080/plaintext -- 16
+```
+
+*Note you may want to tweak the number of client threads (the `-t` arg) being used based on the specs of your load
+generation machine.*

+ 66 - 0
frameworks/Java/vertx-web/benchmark_config.json

@@ -0,0 +1,66 @@
+{
+  "framework": "vertx-web",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "None",
+      "framework": "vertx-web",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "vertx",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "vertx-web",
+      "notes": "",
+      "versus": "vertx"
+    },
+    "mongodb": {
+      "setup_file": "setup",
+      "db_url": "/mongo/db",
+      "query_url": "/mongo/queries?queries=",
+      "fortune_url": "/mongo/fortunes",
+      "update_url": "/mongo/update?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "MongoDB",
+      "framework": "vertx-web",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "vertx",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "vertx-web-mongodb",
+      "notes": "",
+      "versus": ""
+    },
+    "postgres": {
+      "setup_file": "setup",
+      "db_url": "/jdbc/db",
+      "query_url": "/jdbc/queries?queries=",
+      "fortune_url": "/jdbc/fortunes",
+      "update_url": "/jdbc/update?queries=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "vertx-web",
+      "language": "Java",
+      "orm": "Raw",
+      "platform": "vertx",
+      "webserver": "None",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "vertx-web-jdbc",
+      "notes": "",
+      "versus": ""
+    }
+  }]
+}

+ 110 - 0
frameworks/Java/vertx-web/pom.xml

@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>io.vertx</groupId>
+  <artifactId>vertx-benchmark</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+
+  <properties>
+    <!-- the main class -->
+    <main.verticle>io.vertx.benchmark.App</main.verticle>
+    <vertx.version>3.2.0</vertx.version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>io.vertx</groupId>
+      <artifactId>vertx-core</artifactId>
+      <version>${vertx.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>io.vertx</groupId>
+      <artifactId>vertx-web</artifactId>
+      <version>${vertx.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>io.vertx</groupId>
+      <artifactId>vertx-mongo-client</artifactId>
+      <version>${vertx.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>io.vertx</groupId>
+      <artifactId>vertx-jdbc-client</artifactId>
+      <version>${vertx.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>io.vertx</groupId>
+      <artifactId>vertx-web-templ-handlebars</artifactId>
+      <version>${vertx.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.postgresql</groupId>
+      <artifactId>postgresql</artifactId>
+      <version>9.4-1206-jdbc42</version>
+    </dependency>
+
+  </dependencies>
+
+  <build>
+
+    <pluginManagement>
+      <plugins>
+        <!-- We specify the Maven compiler plugin as we need to set it to Java 1.8 -->
+        <plugin>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>3.1</version>
+          <configuration>
+            <source>1.8</source>
+            <target>1.8</target>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <!--
+    You only need the part below if you want to build your application into a fat executable jar.
+    This is a jar that contains all the dependencies required to run it, so you can just run it with
+    java -jar
+    -->
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>2.3</version>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <transformers>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                  <manifestEntries>
+                    <Main-Class>io.vertx.core.Launcher</Main-Class>
+                    <Main-Verticle>${main.verticle}</Main-Verticle>
+                  </manifestEntries>
+                </transformer>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
+                  <resource>META-INF/services/io.vertx.core.spi.VerticleFactory</resource>
+                </transformer>
+              </transformers>
+              <artifactSet>
+              </artifactSet>
+              <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar</outputFile>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>

+ 34 - 0
frameworks/Java/vertx-web/scripts/pipeline.lua

@@ -0,0 +1,34 @@
+local pipelineDepth = 1
+local counter = 0
+local maxRequests = -1
+
+function init(args)
+   if args[1] ~= nil then
+      pipelineDepth = tonumber(args[1])
+   end
+
+   local r = {}
+   for i = 1, pipelineDepth, 1 do
+      r[i] = wrk.format(nil)
+   end
+
+   print("Pipeline depth: " .. pipelineDepth)
+
+   if args[2] ~= nil then
+      maxRequests = tonumber(args[2])
+      print("Max requests: " .. maxRequests)
+   end
+
+   req = table.concat(r)
+end
+
+function request()
+   return req
+end
+
+function response()
+   if counter == maxRequests then
+     wrk.thread:stop()
+   end
+   counter = counter + 1
+end

+ 9 - 0
frameworks/Java/vertx-web/setup.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+sed -i 's|localhost|'"${DBHOST}"'|g' src/main/conf/config.json
+
+fw_depends java8 maven
+
+mvn clean package
+
+java -Xms2G -Xmx2G -server -XX:+UseNUMA -XX:+UseParallelGC -XX:+AggressiveOpts -Dvertx.disableWebsockets=true -Dvertx.flashPolicyHandler=false -Dvertx.threadChecks=false -Dvertx.disableContextTimings=true -Dvertx.disableTCCL=true -jar target/vertx-benchmark-1.0.0-SNAPSHOT-fat.jar --conf src/main/conf/config.json &

+ 15 - 0
frameworks/Java/vertx-web/source_code

@@ -0,0 +1,15 @@
+./vertx-web/src
+./vertx-web/src/main
+./vertx-web/src/main/resources
+./vertx-web/src/main/resources/templates
+./vertx-web/src/main/resources/templates/fortunes.hbs
+./vertx-web/src/main/java
+./vertx-web/src/main/java/io
+./vertx-web/src/main/java/io/vertx
+./vertx-web/src/main/java/io/vertx/benchmark
+./vertx-web/src/main/java/io/vertx/benchmark/App.java
+./vertx-web/src/main/java/io/vertx/benchmark/Helper.java
+./vertx-web/src/main/java/io/vertx/benchmark/model
+./vertx-web/src/main/java/io/vertx/benchmark/model/Message.java
+./vertx-web/src/main/java/io/vertx/benchmark/model/Fortune.java
+./vertx-web/src/main/java/io/vertx/benchmark/model/World.java

+ 9 - 0
frameworks/Java/vertx-web/src/main/conf/config.json

@@ -0,0 +1,9 @@
+{
+  "connection_string": "mongodb://localhost:27017",
+  "db_name": "hello_world",
+
+  "url": "jdbc:postgresql://localhost:5432/hello_world",
+  "driver_class": "org.postgresql.Driver",
+  "user": "benchmarkdbuser",
+  "password": "benchmarkdbpass"
+}

+ 457 - 0
frameworks/Java/vertx-web/src/main/java/io/vertx/benchmark/App.java

@@ -0,0 +1,457 @@
+package io.vertx.benchmark;
+
+import io.vertx.benchmark.model.Fortune;
+import io.vertx.benchmark.model.Message;
+import io.vertx.benchmark.model.World;
+import io.vertx.core.*;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.jdbc.JDBCClient;
+import io.vertx.ext.mongo.MongoClient;
+import io.vertx.ext.sql.SQLConnection;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.templ.HandlebarsTemplateEngine;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+public class App extends AbstractVerticle {
+
+  /**
+   * MongoDB implementation
+   */
+  private final class MongoDB {
+    private final JsonObject FIELDS = new JsonObject().put("_id", 0);
+
+    private final MongoClient database;
+    // In order to use a template we first need to create an engine
+    private final HandlebarsTemplateEngine engine;
+
+    public MongoDB(Vertx vertx, JsonObject config) {
+      this.database = MongoClient.createShared(vertx, config);
+      this.engine = HandlebarsTemplateEngine.create();
+    }
+
+    public final void dbHandler(final RoutingContext ctx) {
+      database.findOne("world", new JsonObject().put("id", Helper.randomWorld()), FIELDS, findOne -> {
+        if (findOne.failed()) {
+          ctx.fail(findOne.cause());
+          return;
+        }
+
+        ctx.response()
+            .putHeader(HttpHeaders.SERVER, SERVER)
+            .putHeader(HttpHeaders.DATE, date)
+            .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
+            .end(new World(findOne.result()).encode());
+      });
+    }
+
+    public final void queriesHandler(final RoutingContext ctx) {
+      final int queries = Helper.getQueries(ctx.request());
+
+      final World[] worlds = new World[queries];
+
+      new Handler<Integer>() {
+        @Override
+        public void handle(Integer idx) {
+          if (idx == queries) {
+            // stop condition
+            ctx.response()
+                .putHeader(HttpHeaders.SERVER, SERVER)
+                .putHeader(HttpHeaders.DATE, date)
+                .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
+                .end(new JsonArray(Arrays.asList(worlds)).encode());
+
+          } else {
+
+            final Handler<Integer> self = this;
+
+            database.findOne("world", new JsonObject().put("id", Helper.randomWorld()), FIELDS, findOne -> {
+              if (findOne.failed()) {
+                ctx.fail(findOne.cause());
+                return;
+              }
+
+              worlds[idx] = new World(findOne.result());
+              self.handle(idx + 1);
+            });
+          }
+        }
+      }.handle(0);
+    }
+
+    public final void fortunesHandler(final RoutingContext ctx) {
+      final List<Fortune> fortunes = new LinkedList<>();
+
+      database.find("fortune", new JsonObject(), find -> {
+        if (find.failed()) {
+          ctx.fail(find.cause());
+          return;
+        }
+
+        for (JsonObject document : find.result()) {
+          fortunes.add(new Fortune(document));
+        }
+
+        fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+        Collections.sort(fortunes);
+
+        ctx.put("fortunes", fortunes);
+
+        // and now delegate to the engine to render it.
+        engine.render(ctx, "templates/fortunes.hbs", res -> {
+          if (res.succeeded()) {
+            ctx.response()
+                .putHeader(HttpHeaders.SERVER, SERVER)
+                .putHeader(HttpHeaders.DATE, date)
+                .putHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
+                .end(res.result());
+          } else {
+            ctx.fail(res.cause());
+          }
+        });
+      });
+    }
+
+    public final void updateHandler(final RoutingContext ctx) {
+      final int queries = Helper.getQueries(ctx.request());
+      final World[] worlds = new World[queries];
+
+      new Handler<Integer>() {
+        @Override
+        public void handle(Integer idx) {
+          if (idx == queries) {
+            // stop condition
+            ctx.response()
+                .putHeader(HttpHeaders.SERVER, SERVER)
+                .putHeader(HttpHeaders.DATE, date)
+                .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
+                .end(new JsonArray(Arrays.asList(worlds)).encode());
+
+          } else {
+
+            final Handler<Integer> self = this;
+
+            final int id = Helper.randomWorld();
+
+            final JsonObject query = new JsonObject().put("id", id);
+
+            database.findOne("world", query, FIELDS, findOne -> {
+              if (findOne.failed()) {
+                ctx.fail(findOne.cause());
+                return;
+              }
+
+              final int newRandomNumber = Helper.randomWorld();
+
+              database.update("world", query, new JsonObject().put("$set", new JsonObject().put("randomNumber", newRandomNumber)), update -> {
+                if (update.failed()) {
+                  ctx.fail(update.cause());
+                  return;
+                }
+
+                worlds[idx] = new World(id, newRandomNumber);
+                self.handle(idx + 1);
+              });
+            });
+
+          }
+        }
+      }.handle(0);
+    }
+  }
+
+  /**
+   * JDBC implementation
+   */
+  private final class JDBC {
+    private final JDBCClient database;
+    // In order to use a template we first need to create an engine
+    private final HandlebarsTemplateEngine engine;
+
+    public JDBC(Vertx vertx, JsonObject config) {
+      this.database = JDBCClient.createShared(vertx, config);
+      this.engine = HandlebarsTemplateEngine.create();
+    }
+
+    public final void dbHandler(final RoutingContext ctx) {
+      database.getConnection(getConnection -> {
+        if (getConnection.failed()) {
+          ctx.fail(getConnection.cause());
+          return;
+        }
+
+        final SQLConnection conn = getConnection.result();
+
+        conn.query("SELECT id, randomnumber from WORLD where id = " + Helper.randomWorld(), query -> {
+          // free the connection
+          conn.close();
+
+          if (query.failed()) {
+            ctx.fail(query.cause());
+            return;
+          }
+
+          final List<JsonArray> resultSet = query.result().getResults();
+
+          if (resultSet == null || resultSet.size() == 0) {
+            ctx.fail(404);
+            return;
+          }
+
+          final JsonArray row = resultSet.get(0);
+
+          ctx.response()
+              .putHeader(HttpHeaders.SERVER, SERVER)
+              .putHeader(HttpHeaders.DATE, date)
+              .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
+              .end(new World(row.getInteger(0), row.getInteger(1)).encode());
+        });
+      });
+    }
+
+    public final void queriesHandler(final RoutingContext ctx) {
+      final int queries = Helper.getQueries(ctx.request());
+      final World[] worlds = new World[queries];
+
+      database.getConnection(getConnection -> {
+        if (getConnection.failed()) {
+          ctx.fail(getConnection.cause());
+          return;
+        }
+
+        final SQLConnection conn = getConnection.result();
+
+        new Handler<Integer>() {
+          @Override
+          public void handle(Integer idx) {
+            if (idx == queries) {
+              // stop condition
+              ctx.response()
+                  .putHeader(HttpHeaders.SERVER, SERVER)
+                  .putHeader(HttpHeaders.DATE, date)
+                  .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
+                  .end(new JsonArray(Arrays.asList(worlds)).encode());
+
+              conn.close();
+            } else {
+
+              final Handler<Integer> self = this;
+
+              conn.query("SELECT id, randomnumber from WORLD where id = " + Helper.randomWorld(), query -> {
+                if (query.failed()) {
+                  ctx.fail(query.cause());
+                  conn.close();
+                  return;
+                }
+
+                final List<JsonArray> resultSet = query.result().getResults();
+
+                if (resultSet == null || resultSet.size() == 0) {
+                  ctx.fail(404);
+                  conn.close();
+                  return;
+                }
+
+                final JsonArray row = resultSet.get(0);
+
+                worlds[idx] = new World(row.getInteger(0), row.getInteger(1));
+                self.handle(idx + 1);
+              });
+            }
+          }
+        }.handle(0);
+      });
+    }
+
+    public final void fortunesHandler(final RoutingContext ctx) {
+      final List<Fortune> fortunes = new LinkedList<>();
+
+      database.getConnection(getConnection -> {
+        if (getConnection.failed()) {
+          ctx.fail(getConnection.cause());
+          return;
+        }
+
+        final SQLConnection conn = getConnection.result();
+
+        conn.query("SELECT id, message from FORTUNE", query -> {
+          // free the connection
+          conn.close();
+
+          if (query.failed()) {
+            ctx.fail(query.cause());
+            return;
+          }
+
+          final List<JsonArray> resultSet = query.result().getResults();
+
+          if (resultSet == null || resultSet.size() == 0) {
+            ctx.fail(404);
+            return;
+          }
+
+          for (JsonArray row : resultSet) {
+            fortunes.add(new Fortune(row.getInteger(0), row.getString(1)));
+          }
+
+          fortunes.add(new Fortune(0, "Additional fortune added at request time."));
+          Collections.sort(fortunes);
+
+          ctx.put("fortunes", fortunes);
+
+          // and now delegate to the engine to render it.
+          engine.render(ctx, "templates/fortunes.hbs", res -> {
+            if (res.succeeded()) {
+              ctx.response()
+                  .putHeader(HttpHeaders.SERVER, SERVER)
+                  .putHeader(HttpHeaders.DATE, date)
+                  .putHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=UTF-8")
+                  .end(res.result());
+            } else {
+              ctx.fail(res.cause());
+            }
+          });
+        });
+      });
+    }
+
+    public final void updateHandler(final RoutingContext ctx) {
+      final int queries = Helper.getQueries(ctx.request());
+      final World[] worlds = new World[queries];
+
+      database.getConnection(getConnection -> {
+        if (getConnection.failed()) {
+          ctx.fail(getConnection.cause());
+          return;
+        }
+
+        final SQLConnection conn = getConnection.result();
+
+        new Handler<Integer>() {
+          @Override
+          public void handle(Integer idx) {
+            if (idx == queries) {
+              // stop condition
+              ctx.response()
+                  .putHeader(HttpHeaders.SERVER, SERVER)
+                  .putHeader(HttpHeaders.DATE, date)
+                  .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
+                  .end(new JsonArray(Arrays.asList(worlds)).encode());
+
+              conn.close();
+            } else {
+
+              final Handler<Integer> self = this;
+              final int id = Helper.randomWorld();
+
+              conn.query("SELECT id, randomnumber from WORLD where id = " + id, query -> {
+                if (query.failed()) {
+                  ctx.fail(query.cause());
+                  conn.close();
+                  return;
+                }
+
+                final List<JsonArray> resultSet = query.result().getResults();
+
+                if (resultSet == null || resultSet.size() == 0) {
+                  ctx.fail(404);
+                  conn.close();
+                  return;
+                }
+
+                final int newRandomNumber = Helper.randomWorld();
+
+                conn.update("UPDATE WORLD SET randomnumber = " + newRandomNumber + " WHERE id = " + id, update -> {
+                  if (update.failed()) {
+                    ctx.fail(update.cause());
+                    conn.close();
+                    return;
+                  }
+
+                  worlds[idx] = new World(id, newRandomNumber);
+                  self.handle(idx + 1);
+                });
+              });
+            }
+          }
+        }.handle(0);
+      });
+    }
+  }
+
+  private static final String SERVER = "vertx-web";
+  private String date;
+
+  @Override
+  public void start() {
+    final Router app = Router.router(vertx);
+
+    vertx.setPeriodic(1000, handler -> date = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now()));
+
+    final MongoDB mongoDB = new MongoDB(vertx, config());
+    final JDBC jdbc = new JDBC(vertx, config());
+
+    /**
+     * This test exercises the framework fundamentals including keep-alive support, request routing, request header
+     * parsing, object instantiation, JSON serialization, response header generation, and request count throughput.
+     */
+    app.get("/json").handler(ctx -> {
+      ctx.response()
+          .putHeader(HttpHeaders.SERVER, SERVER)
+          .putHeader(HttpHeaders.DATE, date)
+          .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
+          .end(new Message("Hello, World!").encode());
+    });
+
+    /**
+     * This test exercises the framework's object-relational mapper (ORM), random number generator, database driver,
+     * and database connection pool.
+     */
+    app.get("/mongo/db").handler(mongoDB::dbHandler);
+    app.get("/jdbc/db").handler(jdbc::dbHandler);
+
+    /**
+     * This test is a variation of Test #2 and also uses the World table. Multiple rows are fetched to more dramatically
+     * punish the database driver and connection pool. At the highest queries-per-request tested (20), this test
+     * demonstrates all frameworks' convergence toward zero requests-per-second as database activity increases.
+     */
+    app.get("/mongo/queries").handler(mongoDB::queriesHandler);
+    app.get("/jdbc/queries").handler(jdbc::queriesHandler);
+
+    /**
+     * This test exercises the ORM, database connectivity, dynamic-size collections, sorting, server-side templates,
+     * XSS countermeasures, and character encoding.
+     */
+    app.get("/mongo/fortunes").handler(mongoDB::fortunesHandler);
+    app.get("/jdbc/fortunes").handler(jdbc::fortunesHandler);
+
+    /**
+     * This test is a variation of Test #3 that exercises the ORM's persistence of objects and the database driver's
+     * performance at running UPDATE statements or similar. The spirit of this test is to exercise a variable number of
+     * read-then-write style database operations.
+     */
+    app.route("/mongo/update").handler(mongoDB::updateHandler);
+    app.route("/jdbc/update").handler(jdbc::updateHandler);
+
+    /**
+     * This test is an exercise of the request-routing fundamentals only, designed to demonstrate the capacity of
+     * high-performance platforms in particular. Requests will be sent using HTTP pipelining. The response payload is
+     * still small, meaning good performance is still necessary in order to saturate the gigabit Ethernet of the test
+     * environment.
+     */
+    app.get("/plaintext").handler(ctx -> {
+      ctx.response()
+          .putHeader(HttpHeaders.SERVER, SERVER)
+          .putHeader(HttpHeaders.DATE, date)
+          .putHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
+          .end("Hello, World!");
+    });
+
+    vertx.createHttpServer().requestHandler(app::accept).listen(8080);
+  }
+}

+ 42 - 0
frameworks/Java/vertx-web/src/main/java/io/vertx/benchmark/Helper.java

@@ -0,0 +1,42 @@
+package io.vertx.benchmark;
+
+import io.vertx.core.http.HttpServerRequest;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+public final class Helper {
+
+  private Helper() {
+  }
+
+  /**
+   * Returns the value of the "queries" getRequest parameter, which is an integer
+   * bound between 1 and 500 with a default value of 1.
+   *
+   * @param request the current HTTP request
+   * @return the value of the "queries" parameter
+   */
+  static int getQueries(HttpServerRequest request) {
+    String param = request.getParam("queries");
+
+    if (param == null) {
+      return 1;
+    }
+    try {
+      int parsedValue = Integer.parseInt(param);
+      return Math.min(500, Math.max(1, parsedValue));
+    } catch (NumberFormatException e) {
+      return 1;
+    }
+  }
+
+  /**
+   * Returns a random integer that is a suitable value for both the {@code id}
+   * and {@code randomNumber} properties of a world object.
+   *
+   * @return a random world number
+   */
+  static int randomWorld() {
+    return 1 + ThreadLocalRandom.current().nextInt(10000);
+  }
+}

+ 42 - 0
frameworks/Java/vertx-web/src/main/java/io/vertx/benchmark/model/Fortune.java

@@ -0,0 +1,42 @@
+package io.vertx.benchmark.model;
+
+import io.vertx.core.json.JsonObject;
+
+import java.util.Collections;
+
+/**
+ * The model for the "fortune" database table.
+ */
+public final class Fortune extends JsonObject implements Comparable<Fortune> {
+
+  private static final String ID = "id";
+  private static final String MESSAGE = "message";
+
+  /**
+   * Constructs a new fortune object with the given parameters.
+   *
+   * @param id the ID of the fortune
+   * @param message the message of the fortune
+   */
+  public Fortune(int id, String message) {
+    put(ID, id);
+    put(MESSAGE, message);
+  }
+
+  public Fortune(JsonObject doc) {
+    super(doc == null ? Collections.emptyMap() : doc.getMap());
+  }
+
+  public int getId() {
+    return getInteger(ID);
+  }
+
+  public String getMessage() {
+    return getString(MESSAGE);
+  }
+
+  @Override
+  public int compareTo(Fortune other) {
+    return getMessage().compareTo(other.getMessage());
+  }
+}

+ 16 - 0
frameworks/Java/vertx-web/src/main/java/io/vertx/benchmark/model/Message.java

@@ -0,0 +1,16 @@
+package io.vertx.benchmark.model;
+
+import io.vertx.core.json.JsonObject;
+
+public class Message extends JsonObject {
+
+  private static final String MESSAGE = "message";
+
+  public Message(String message) {
+    put(MESSAGE, message);
+  }
+
+  public String getMessage() {
+    return getString(MESSAGE);
+  }
+}

+ 37 - 0
frameworks/Java/vertx-web/src/main/java/io/vertx/benchmark/model/World.java

@@ -0,0 +1,37 @@
+package io.vertx.benchmark.model;
+
+import io.vertx.core.json.JsonObject;
+
+import java.util.Collections;
+
+/**
+ * The model for the "world" database table.
+ */
+public final class World extends JsonObject {
+
+  public static final String ID = "id";
+  public static final String RANDOM_NUMBER = "randomNumber";
+
+  /**
+   * Constructs a new world object with the given parameters.
+   *
+   * @param id the ID of the world
+   * @param randomNumber the random number of the world
+   */
+  public World(int id, int randomNumber) {
+    put(ID, id);
+    put(RANDOM_NUMBER, randomNumber);
+  }
+
+  public World(JsonObject doc) {
+    super(doc == null ? Collections.emptyMap() : doc.getMap());
+  }
+
+  public int getId() {
+    return getInteger(ID);
+  }
+
+  public int getRandomNumber() {
+    return getInteger(RANDOM_NUMBER);
+  }
+}

+ 15 - 0
frameworks/Java/vertx-web/src/main/resources/templates/fortunes.hbs

@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+  <tr>
+    <th>id</th>
+    <th>message</th>
+  </tr> {{#each fortunes}}
+  <tr>
+    <td>{{id}}</td>
+    <td>{{message}}</td>
+  </tr> {{/each}} </table>
+</body>
+</html>