June 21, 2018

TechEmpower framework benchmarks & Duct

Hey people of the internet, in order to continue our serie on Clojure & Duct, I decided to implement the TechEmpower frameworks benchmark this time using the Duct framework, as it is not already listed in Round 16 and I saw no Pull Request about it on their Github, and because this is fun & quick.

TechEmpower frameworks benchmark

This project provides representative performance measures across a wide field of web application frameworks. With much help from the community, coverage is quite broad and we are happy to broaden it further with contributions. The project presently includes frameworks on many languages including Go, Python, Java, Ruby, PHP, C#, Clojure, Groovy, Dart, JavaScript, Erlang, Haskell, Scala, Perl, Lua, C, and others. The current tests exercise plaintext responses, JSON seralization, database reads and writes via the object-relational mapper (ORM), collections, sorting, server-side templates, and XSS counter-measures. Future tests will exercise other components and greater computation.

As of today, the project have been implemented in Clojure with:

  • aleph
  • compojure
  • htt-kit
  • immutant
  • luminus
  • macchiato
  • pedestal
  • reitit

and if it ships with Round 17, duct will come to the party.

This is not my first time, I already added support for the Martini, Goji & go-json-rest frameworks in 2014.

The current implementation for duct is on my GitHub, don’t hesitate to fork & pull request if you see any improvement or bug :)

Implementation

Scaffolding

Type lein new duct hello +ataraxy +postgres and we’re good to go. We’ll directly update the dependencies in our project.clj file so that we’re ready for the various implementations.

project.clj

  :dependencies [[org.clojure/clojure "1.9.0"]
                 [duct/core "0.6.2"]
                 [duct/module.logging "0.3.1"]
                 [duct/module.web "0.6.4"]
                 [duct/module.ataraxy "0.2.0"]
                 [duct/module.sql "0.4.2"]
                 [duct/database.sql.hikaricp "0.3.3"]
                 [org.postgresql/postgresql "42.1.4"]
                 [hiccup "1.0.5"]]

We just added duct/database.sql.hikaricp and hiccup.

In order to run in local since I already have a Postgres installed, I chose that. Edit your dev.edn file so that it looks like this

dev.edn

{:duct.core/environment :development
 :duct.core/include ["hello/config"]

 :duct.module/sql
 {:database-url "jdbc:postgresql://localhost:5432/benchmark"}}

Then connect to your Postgres, create a database named benchmark and create two tables World and Fortune

create database benchmark;
\connect benchmark
create table World(id int primary key, randomNumber int);
create table Fortune(id int primary key, message varchar(255));

Now open your REPL and be ready.

JSON

In order to implement the JSON endpoint whose job is to return {message: "Hello, World!"} we’re going to use ataraxy and the duct.handler.static/ok feature.

Open your config.edn file and add the following:

 :duct.module/ataraxy
 {[:get "/json"] [:json]}

 [:duct.handler.static/ok :hello.handler/json]
 {:body {:message "Hello, World!"}}

That’s it, it is implemented. We declared in ataraxy a route for GET on uri /json that is to be referenced by the name :json then we declared a composite key [:duct.handler.static/ok :hello.handler/json] (here :hello.handler/json is our :json just from above). This composite key returns a body of type map, that duct will serialize to JSON automatically.

Type (reset) in your REPL and in a second terminal type http :3000/json you should be greeted with the correct JSON.

Plain text

The implementation is time is almost the same. Open your config.edn file and edit/add the following.

:duct.module/ataraxy
{[:get "/json"]      [:json]
 [:get "/plaintext"] [:plaintext]}

[:duct.handler.static/ok :hello.handler/json]
{:body {:message "Hello, World!"}}

[:duct.handler.static/okplain :hello.handler/plaintext]
{:body "Hello, World!"}  

We’re going to create this duct.handler.static/okplain because it is not included in duct. In order to do that create a new file src/hello/handler/plaintext.clj and type te following code.

src/hello/handler/plaintext.clj

(ns hello.handler.plaintext
  (:require [integrant.core :as ig]
            [ataraxy.handler :as handler]
            [ataraxy.response :as ataraxy-resp]
            [ring.util.response :as resp]))

; Create a specific init-key extending ::ok, patching the Content-Type header
; so that it equals "text/plain" instead of "application/octet-stream"
(defmethod ig/init-key :duct.handler.static/okplain [_ response]
  (let [val (ig/init-key :duct.handler.static/ok response)
        patched (update-in (val response) [:headers "Content-Type"] (constantly "text/plain"))]
    (constantly patched)))

In this file we create a new ig/init-key multi-method implementation for the :duct.handler.static/okplain specific keyword.

The implementation calls the ig/init-key implementation for :duct.handler.static/ok which augments the response with a :status 200, and patch the response by adding in :headers and Content-Type which is to be text/plain.

Finally since the result of ig/init-key is a function, we return constantly this patched version of the response.

Go back in your second terminal type http :3000/plaintext you should be greeted with the correct plain text message, take a look at the Content-Type and make sure it’s text/plain.

Without patching this, it would have been application/octet-stream. This is specific to what the TechEmpower frameworks benchmark expect.

Database queries

In the first part we created the table World. The goal of the 2 next tests is to execute a request on the database. The first one will execute a single query by searching with a random id. The second one will get a URL parameter to configure how much times it has to do that.

We’ll start by adding our Boundary in src/hello/boundary/world_db.clj

src/hello/boundary/world_db.clj

(ns hello.boundary.world-db
  (:require [duct.database.sql]
            [clojure.java.jdbc :as jdbc]))

(defn- query [db]
  (first
    (jdbc/query db ["select * from world where id = ?" (inc (rand-int 9999))])))

(defprotocol World
  (make-single-query [db])
  (make-multiple-queries [db num]))

(extend-protocol World
  duct.database.sql.Boundary

  (make-single-query [{:keys [spec]}]
    (query spec))

  (make-multiple-queries [{:keys [spec]} num]
    (repeatedly num #(query spec))))

The implementation defines a function named query which execute a SQL query looking for a specific id which will be a random integer between 1 and 10 000

Our World protocol have two methods make-single-query and make-multiple-queries which will do the same but multiple times.

By extending the World protocol and extending duct.database.sql.Boundary we can retrieve the database spec and the two implementation are just calling the query method, or repeatedly calling it.

Having created the Boundary, we now just need to use it.

Add the needed config to config.edn:

:duct.module/ataraxy
 {[:get "/json"]                 [:json]
  [:get "/plaintext"]            [:plaintext]
  [:get "/db"]                   [:single-query]
  [:get "/queries" #{?queries}]  [:queries]}

; ...

:hello.handler/single-query
{:db #ig/ref :duct.database/sql}

:hello.handler/queries
{:db #ig/ref :duct.database/sql}  

Create a new file named src/hello/handler/single_query.clj and implement the functionality.

src/hello/handler/single_query.clj

(ns hello.handler.single-query
  (:require [integrant.core :as ig]
            [ataraxy.response :as response]
            [hello.boundary.world-db :as world-db]))

(defmethod ig/init-key :hello.handler/single-query [_ {:keys [db]}]
  (fn [{[_] :ataraxy/result}]
    [::response/ok (world-db/make-single-query db)]))

We just import our Boundary and use it to respond to the client. That’s all there is to it.

Create a new file named src/hello/handler/queries.clj and implement the functionality.

src/hello/handler/queries.clj

(ns hello.handler.queries
  (:require [integrant.core :as ig]
            [ataraxy.response :as response]
            [hello.boundary.world-db :as world-db]))

; taken from the luminus sample
(defn query-count
  "Parse provided string value of query count, clamping values to between 1 and 500."
  [^String queries]
  (let [n ^long (try (Integer/parseInt queries)
                     (catch Exception _ 1))] ; default to 1 on parse failure
    (cond
      (< ^long n 1) 1
      (> ^long n 500) 500
      :else n)))

(defmethod ig/init-key :hello.handler/queries [_ {:keys [db]}]
  (fn [request]
    [::response/ok
     (let [queries (get-in request [:params :queries] "1")
           num (query-count queries)]
       (world-db/make-multiple-queries db num))]))

As you’ve seen I took the implementation of query-count from the Luminus implementation sample. It just checks that a parameter queries can be parsed as an integer, if so it will try to bound it between 1 and 500, otherwise it will default to 1.

The duct implementation extract the :queries parameter from the request and give it to our Boundary so that it can loop on it that much times.

Go back in your second REPL and hit http :3000/db and http :3000/queries?queries=3 and you should have the desired result.

Fortunes

In the first part we created the table Fortune. The goal of this test is to execute select * on the database, format the messages as HTML and return it to the client.

We’ll start by adding our Boundary in src/hello/boundary/fortune_db.clj

src/hello/boundary/world_db.clj

(ns hello.boundary.fortune-db
  (:require [duct.database.sql]
            [clojure.java.jdbc :as jdbc]))

(defprotocol Fortune
  (get-all [db]))

(extend-protocol Fortune
  duct.database.sql.Boundary

  (get-all [{:keys [spec]}]
    (jdbc/query spec ["select * from fortune"])))

Nothing specific, the get-all function just executes a select * from fortune.

Now that we have our Boundary we need to use it to implement the functionality. Open the config.edn file and modify it.

 :duct.module/ataraxy
 {[:get "/json"]                 [:json]
  [:get "/plaintext"]            [:plaintext]
  [:get "/db"]                   [:single-query]
  [:get "/queries" #{?queries}]  [:queries]
  [:get "/fortunes"]             [:fortunes]}

 ; ...

 :hello.handler/fortunes
 {:db #ig/ref :duct.database/sql}} 

It’s all for the configuration, but we still need to implement the :hello.handler/fortunes.

Create a new file named src/hello/handler/fortunes.clj

src/hello/handler/fortunes.clj

(ns hello.handler.fortunes
  (:require [integrant.core :as ig]
            [ataraxy.response :as response]
            [hello.boundary.fortune-db :as fortune-db]))

; copied from pedestal implementation
(defn prepare-fortunes
  [fortunes]
  (sort-by :message
           (conj fortunes
                 {:id 0 :message "Additional fortune added at request time."})))

; ------- raw string -------

(def ^String base-fortune-pre "<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>")
(def ^String base-fortune-post "</table></body></html>")
(defn fortunes-str
  "The HTML bit for this is very very small;
  Opt to create the HTML string by hand in a tight loop rather than using Hiccup"
  [fortunes]
  (let [sb (StringBuilder. ^String base-fortune-pre)]
    (doseq [{:keys [id message]} fortunes]
      (.append sb "<tr><td>")
      (.append sb (str id))
      (.append sb "</td><td>")
      (dotimes [c-idx (count message)]
        (let [c (.charAt ^String message c-idx)]
          (case c
            \& (.append sb "&")
            \" (.append sb """)
            \' (.append sb "'")
            \< (.append sb "<")
            \> (.append sb ">")
            (.append sb c))))
      (.append sb "</td></tr>"))
    (.append sb base-fortune-post)
    (.toString sb)))

(defmethod ig/init-key :hello.handler/fortunes [_ {:keys [db]}]
  (fn [{[_] :ataraxy/result}]
    (let [fortunes (prepare-fortunes (fortune-db/get-all db))]
      [::response/ok (fortunes-str fortunes)])))

The important parts are the prepare-fortunes and fortune-db/get-all function which are combined to create our dataset that have to be rendered as HTML. The implementation with StringBuilder have been copied from the pedestal implementation.

Done

On my repository and pull request I also implemented these tests using MongoDB, and running on various server implementations like Jetty, Aleph, HTTP-Kit and Immutant so that we know which combination is faster.

As always you can see the whole code on my Github

Alexandre Grison - //grison.me - @algrison