From ebd3d8e7978e7cd42073ff72bb42ed7dfd37b3ab Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 7 Dec 2015 08:58:00 +0200 Subject: [PATCH 1/3] Add performance baseline tests --- project.clj | 3 + test/compojure/api/perf_test.clj | 101 +++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 test/compojure/api/perf_test.clj diff --git a/project.clj b/project.clj index 276dbced..e086a417 100644 --- a/project.clj +++ b/project.clj @@ -37,12 +37,14 @@ [com.stuartsierra/component "0.3.1"] [reloaded.repl "0.2.1"] [http-kit "2.1.19"] + [criterium "0.4.3"] ; Required when using with Java 1.6 [org.codehaus.jsr166-mirror/jsr166y "1.7.0"]] :ring {:handler examples.thingie/app :reload-paths ["src" "examples/src"]} :source-paths ["examples/src" "examples/dev-src"] :main examples.server} + :perf {:jvm-opts ^:replace []} :logging {:dependencies [[org.clojure/tools.logging "0.3.1"]]} :1.8 {:dependencies [[org.clojure/clojure "1.8.0-RC2"]]}} :eastwood {:namespaces [:source-paths] @@ -55,5 +57,6 @@ "start-thingie" ["run"] "aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"] "test-ancient" ["midje"] + "perf" ["with-profile" "default,dev,perf"] "deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."} ["do" ["clean"] ["midje"] ["deploy" "clojars"]]}) diff --git a/test/compojure/api/perf_test.clj b/test/compojure/api/perf_test.clj new file mode 100644 index 00000000..d6567ed2 --- /dev/null +++ b/test/compojure/api/perf_test.clj @@ -0,0 +1,101 @@ +(ns compojure.api.perf-test + (:require [compojure.api.sweet :refer :all] + [compojure.api.test-utils :refer :all] + [criterium.core :as cc] + [midje.sweet :refer :all] + [ring.util.http-response :refer :all] + [schema.core :as s])) + +(defn title [s] + (println + (str "\n\u001B[35m" + (apply str (repeat (+ 6 (count s)) "#")) + "\n## " s " ##\n" + (apply str (repeat (+ 6 (count s)) "#")) + "\u001B[0m\n"))) + +(s/defschema Order {:id s/Str + :name s/Str + (s/optional-key :description) s/Str + :address (s/maybe {:street s/Str + :country (s/enum "FI" "PO")}) + :orders [{:name s/Str + :price s/Any + :shipping s/Bool}]}) + +(defn c-api-bench [] + + (let [app (api + (GET* "/30" [] + (ok {:result 30}))) + call #(get* app "/30")] + + (title "GET JSON") + + (assert (= {:result 30} (second (call)))) + (cc/quick-bench (call))) + + ; 32µs + + (let [app (api + (POST* "/plus" [] + :return {:result s/Int} + :body-params [x :- s/Int, y :- s/Int] + (ok {:result (+ x y)}))) + data (json {:x 10, :y 20}) + call #(post* app "/plus" data)] + + (title "JSON POST with 2-way coercion") + + (assert (= {:result 30} (second (call)))) + (cc/quick-bench (call))) + + ;; 104µs + + (let [app (api + (context* "/a" [] + (context* "/b" [] + (context* "/c" [] + (POST* "/plus" [] + :return {:result s/Int} + :body-params [x :- s/Int, y :- s/Int] + (ok {:result (+ x y)})))))) + data (json {:x 10, :y 20}) + call #(post* app "/a/b/c/plus" data)] + + (title "JSON POST with 2-way coercion + contexts") + + (assert (= {:result 30} (second (call)))) + (cc/quick-bench (call))) + + ;; 113µs + + (let [app (api + (POST* "/echo" [] + :return Order + :body [order Order] + (ok order))) + data (json {:id "123" + :name "Tommi's order" + :description "Totally great order" + :address {:street "Randomstreet 123" + :country "FI"} + :orders [{:name "k1" + :price 123.0 + :shipping true} + {:name "k2" + :price 42.0 + :shipping false}]}) + call #(post* app "/echo" data)] + + (title "JSON POST with nested data") + + (s/check Order (second (call))) + (cc/quick-bench (call))) + + ;; 343µs + + ) + + +(c-api-bench) From 2077c142cc0b6fa0d84a9921ebb57e5c525e0bdf Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 7 Dec 2015 09:00:29 +0200 Subject: [PATCH 2/3] Memoize coercers for better performance --- src/compojure/api/meta.clj | 17 +++++++++++------ test/compojure/api/perf_test.clj | 18 +++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/compojure/api/meta.clj b/src/compojure/api/meta.clj index 3ce5577a..ced06d2e 100644 --- a/src/compojure/api/meta.clj +++ b/src/compojure/api/meta.clj @@ -7,11 +7,12 @@ [plumbing.core :refer :all] [plumbing.fnk.impl :as fnk-impl] [ring.swagger.common :refer :all] - [ring.swagger.schema :as schema] [ring.swagger.json-schema :as js] [ring.util.http-response :refer [internal-server-error]] [slingshot.slingshot :refer [throw+]] [schema.core :as s] + [schema.coerce :as sc] + [schema.utils :as su] [schema-tools.core :as st])) ;; @@ -23,7 +24,7 @@ '+compojure-api-request+) (def +compojure-api-meta+ - "lexically bound meta-data for handlers. EXPERIMENTAL." + "lexically bound meta-data for handlers." '+compojure-api-meta+) (defmacro meta-container [meta & form] @@ -46,6 +47,8 @@ ;; Schema ;; +(def memoized-coercer (memoize sc/coercer)) + (defn strict [schema] (dissoc schema 'schema.core/Keyword)) @@ -59,8 +62,9 @@ (if-let [{:keys [status] :as response} (handler request)] (if-let [schema (:schema (responses status))] (if-let [matcher (:response (mw/get-coercion-matcher-provider request))] - (let [body (schema/coerce schema (:body response) matcher)] - (if (schema/error? body) + (let [coerce (memoized-coercer (value-of schema) matcher) + body (coerce (:body response))] + (if (su/error? body) (throw+ (assoc body :type ::ex/response-validation)) (assoc response ::serializable? true @@ -75,8 +79,9 @@ (assert (not (#{:query :json} type)) (str type " is DEPRECATED since 0.22.0. Use :body or :string instead.")) `(let [value# (keywordize-keys (~key ~+compojure-api-request+))] (if-let [matcher# (~type (mw/get-coercion-matcher-provider ~+compojure-api-request+))] - (let [result# (schema/coerce ~schema value# matcher#)] - (if (schema/error? result#) + (let [coerce# (memoized-coercer ~schema matcher#) + result# (coerce# value#)] + (if (su/error? result#) (throw+ (assoc result# :type ::ex/request-validation)) result#)) value#))) diff --git a/test/compojure/api/perf_test.clj b/test/compojure/api/perf_test.clj index d6567ed2..b24f5b2f 100644 --- a/test/compojure/api/perf_test.clj +++ b/test/compojure/api/perf_test.clj @@ -2,10 +2,14 @@ (:require [compojure.api.sweet :refer :all] [compojure.api.test-utils :refer :all] [criterium.core :as cc] - [midje.sweet :refer :all] [ring.util.http-response :refer :all] [schema.core :as s])) +;; +;; start repl with `lein perf repl`. note, all numbers are from Tommi's +;; laptop. +;; + (defn title [s] (println (str "\n\u001B[35m" @@ -35,7 +39,7 @@ (assert (= {:result 30} (second (call)))) (cc/quick-bench (call))) - ; 32µs + ; 32µs => 30µs (-6%) (let [app (api (POST* "/plus" [] @@ -50,7 +54,7 @@ (assert (= {:result 30} (second (call)))) (cc/quick-bench (call))) - ;; 104µs + ;; 104µs => 73µs (-30%) (let [app (api (context* "/a" [] @@ -68,7 +72,7 @@ (assert (= {:result 30} (second (call)))) (cc/quick-bench (call))) - ;; 113µs + ;; 113µs => 80µs (-30%) (let [app (api (POST* "/echo" [] @@ -93,9 +97,9 @@ (s/check Order (second (call))) (cc/quick-bench (call))) - ;; 343µs + ;; 343µs => 175µs (-49%) ) - -(c-api-bench) +(comment + (c-api-bench)) From cd778adcd58b614899949699ffd89164898c4712 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 7 Dec 2015 21:48:24 +0200 Subject: [PATCH 3/3] Rerun tests with bench (up to +40% perf) --- test/compojure/api/perf_test.clj | 37 +++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/test/compojure/api/perf_test.clj b/test/compojure/api/perf_test.clj index b24f5b2f..ad01669c 100644 --- a/test/compojure/api/perf_test.clj +++ b/test/compojure/api/perf_test.clj @@ -6,8 +6,18 @@ [schema.core :as s])) ;; -;; start repl with `lein perf repl`. note, all numbers are from Tommi's -;; laptop. +;; start repl with `lein perf repl` +;; perf measured with the following setup: +;; +;; Model Name: MacBook Pro +;; Model Identifier: MacBookPro11,3 +;; Processor Name: Intel Core i7 +;; Processor Speed: 2,5 GHz +;; Number of Processors: 1 +;; Total Number of Cores: 4 +;; L2 Cache (per Core): 256 KB +;; L3 Cache: 6 MB +;; Memory: 16 GB ;; (defn title [s] @@ -23,11 +33,12 @@ (s/optional-key :description) s/Str :address (s/maybe {:street s/Str :country (s/enum "FI" "PO")}) - :orders [{:name s/Str + :orders [{:name #"^k" :price s/Any :shipping s/Bool}]}) -(defn c-api-bench [] +(defn bench [] + (let [app (api (GET* "/30" [] @@ -37,9 +48,9 @@ (title "GET JSON") (assert (= {:result 30} (second (call)))) - (cc/quick-bench (call))) + (cc/bench (call))) - ; 32µs => 30µs (-6%) + ; 26µs => 26µs (-0%) (let [app (api (POST* "/plus" [] @@ -52,9 +63,9 @@ (title "JSON POST with 2-way coercion") (assert (= {:result 30} (second (call)))) - (cc/quick-bench (call))) + (cc/bench (call))) - ;; 104µs => 73µs (-30%) + ;; 87µs => 65µs (-25%) (let [app (api (context* "/a" [] @@ -70,9 +81,9 @@ (title "JSON POST with 2-way coercion + contexts") (assert (= {:result 30} (second (call)))) - (cc/quick-bench (call))) + (cc/bench (call))) - ;; 113µs => 80µs (-30%) + ;; 102µs => 78µs (-24%) (let [app (api (POST* "/echo" [] @@ -95,11 +106,11 @@ (title "JSON POST with nested data") (s/check Order (second (call))) - (cc/quick-bench (call))) + (cc/bench (call))) - ;; 343µs => 175µs (-49%) + ;; 311µs => 194µs (-38%) ) (comment - (c-api-bench)) + (bench))