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/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 new file mode 100644 index 00000000..ad01669c --- /dev/null +++ b/test/compojure/api/perf_test.clj @@ -0,0 +1,116 @@ +(ns compojure.api.perf-test + (:require [compojure.api.sweet :refer :all] + [compojure.api.test-utils :refer :all] + [criterium.core :as cc] + [ring.util.http-response :refer :all] + [schema.core :as s])) + +;; +;; 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] + (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 #"^k" + :price s/Any + :shipping s/Bool}]}) + +(defn bench [] + + + (let [app (api + (GET* "/30" [] + (ok {:result 30}))) + call #(get* app "/30")] + + (title "GET JSON") + + (assert (= {:result 30} (second (call)))) + (cc/bench (call))) + + ; 26µs => 26µs (-0%) + + (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/bench (call))) + + ;; 87µs => 65µs (-25%) + + (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/bench (call))) + + ;; 102µs => 78µs (-24%) + + (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/bench (call))) + + ;; 311µs => 194µs (-38%) + + ) + +(comment + (bench))