Skip to content

Commit

Permalink
Add new compile options to enable optimizations (#385)
Browse files Browse the repository at this point in the history
* Add new performance-related compile options

* Add new benchmarks where optimizations are enabled

* Fix typo
  • Loading branch information
hlship committed Oct 29, 2021
1 parent 709138c commit cf2d178
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 95 deletions.
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ A small incompatible change is present: `com.walmartlabs.resolve/resolve-as` an
support an argument that could be either a single error map, or a sequence of error maps. This was rarely used, and
has been changed: only a single error map is supported.

New options have been added to `com.walmartlabs.lacina.schema/compile` that turn off certain checks and features
to boost performance.

[Closed Issues](https://github.com/walmartlabs/lacinia/milestone/28?closed=1)


Expand Down
99 changes: 50 additions & 49 deletions dev-resources/perf.clj
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
(ns perf
"A namespace where we track relative performance of query parsing and execution."
(:require
[org.example.schema :refer [star-wars-schema]]
[criterium.core :as c]
[com.walmartlabs.lacinia :refer [execute execute-parsed-query]]
[com.walmartlabs.lacinia.parser.schema :refer [parse-schema]]
[com.walmartlabs.lacinia.parser :as parser]
[clojure.java.shell :refer [sh]]
[clojure.string :as str]
[clojure.tools.cli :as cli]
[clojure.data.json :as json]
[clojure.data.csv :as csv]
[com.walmartlabs.lacinia.util :as util]
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia.resolve :refer [resolve-as]]
[com.walmartlabs.lacinia.executor :as executor]
[clojure.java.io :as io]
[clojure.pprint :as pprint]
[com.walmartlabs.test-utils :refer [simplify]]
[clojure.edn :as edn]
[clj-async-profiler.core :as prof]
[com.walmartlabs.lacinia.resolve :as resolve])
[org.example.schema :refer [star-wars-schema]]
[criterium.core :as c]
[com.walmartlabs.lacinia :refer [execute execute-parsed-query]]
[com.walmartlabs.lacinia.parser.schema :refer [parse-schema]]
[com.walmartlabs.lacinia.parser :as parser]
[clojure.java.shell :refer [sh]]
[clojure.string :as str]
[clojure.tools.cli :as cli]
[clojure.data.json :as json]
[clojure.data.csv :as csv]
[com.walmartlabs.lacinia.util :as util]
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia.resolve :refer [resolve-as]]
[com.walmartlabs.lacinia.executor :as executor]
[clojure.java.io :as io]
[clojure.pprint :as pprint]
[com.walmartlabs.test-utils :refer [simplify]]
[clojure.edn :as edn]
[clj-async-profiler.core :as prof]
[com.walmartlabs.lacinia.resolve :as resolve])
(:import
(java.util Date)
(java.util.concurrent ThreadPoolExecutor TimeUnit LinkedBlockingQueue)))
(java.util Date)
(java.util.concurrent ThreadPoolExecutor TimeUnit LinkedBlockingQueue)))

(defn -defeat-linter
[]
Expand All @@ -47,25 +47,26 @@
(defn ^:private read-edn
[file]
(or (some-> (io/resource file)
slurp
edn/read-string)
(throw (ex-info "File not found" {:file file}))))
slurp
edn/read-string)
(throw (ex-info "File not found" {:file file}))))

(defn ^:private read-json
[file]
(or (some-> file
io/resource
slurp
(json/read-str :key-fn keyword))))
io/resource
slurp
(json/read-str :key-fn keyword))))

(def compiled-deep-schema
(-> "deep.sdl"
io/resource
slurp
parse-schema
(util/inject-resolvers {:Query/getResource (constantly
(read-json "StructureDefinition-us-core-pulse-oximetry.json"))})
schema/compile))
io/resource
slurp
parse-schema
(util/inject-resolvers {:Query/getResource (constantly
(read-json "StructureDefinition-us-core-pulse-oximetry.json"))})
(schema/compile {:disable-checks? true
:disable-java-objects? true})))

;; A schema to measure the performance of errors
(def planets-schema
Expand Down Expand Up @@ -100,12 +101,12 @@
{:name "Uranus"}]
resolvers {:base-name (fn [_ _ base]
(resolve-as (:name base)
(cond
(:alien? base)
{:message "Europa is forbidden. Attempt no landing there."}
(cond
(:alien? base)
{:message "Europa is forbidden. Attempt no landing there."}

(:destroyed? base)
{:message "This base has been destroyed."})))
(:destroyed? base)
{:message "This base has been destroyed."})))
:planets (fn [_ _ _]
planet-data)}]
(-> '{:objects
Expand Down Expand Up @@ -455,9 +456,9 @@
parse-double (fn [row ix]
(update row ix #(Double/parseDouble %)))]
(into [header-row]
(->> raw-data
(mapv #(parse-double % 3))
(mapv #(parse-double % 4))))))
(->> raw-data
(mapv #(parse-double % 3))
(mapv #(parse-double % 4))))))

(defn ^:private run-benchmarks
[options]
Expand All @@ -466,7 +467,7 @@
(let [prefix [(format "%tY%<tm%<td" (Date.))
(or (:commit options) (git-commit))]
new-benchmarks (->> (map run-benchmark (keys benchmark-queries))
(map #(into prefix %)))
(map #(into prefix %)))
dataset (into (read-dataset) new-benchmarks)]

(when-not (:no-print options)
Expand Down Expand Up @@ -499,7 +500,7 @@
(run! println errors)))]
(cond
(or (:help options)
(seq errors))
(seq errors))
(usage errors)

:else
Expand All @@ -515,12 +516,12 @@
[tree]
(mapcat (fn [[field-name instances]]
(let [fields (->> instances
(mapv #(vector field-name (:args %))))
(mapv #(vector field-name (:args %))))
sub-selections (->> instances
(map :selections)
(mapcat selection-tree->field-tuples))]
(map :selections)
(mapcat selection-tree->field-tuples))]
(into fields sub-selections)))
tree))
tree))


(defn test-parallel
Expand All @@ -531,7 +532,7 @@
(doall
(pmap (fn [x]
(test-benchmark benchmark-name))
(range n)))
(range n)))
nil)))

(comment
Expand Down
5 changes: 5 additions & 0 deletions perf/benchmarks.csv
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,8 @@ date,commit,kind,parse,exec
20211029,20095c09,basic,0.173101540765391,0.13581221989528794
20211029,20095c09,basic-vars,0.23765225940860218,0.15242919057971016
20211029,20095c09,errors,0.09199271510747806,3.186511141414142
20211029,6a458c7c,introspection,0.6659131884057972,1.4485926666666669
20211029,6a458c7c,deep,1.1955770337078653,7.93822526923077
20211029,6a458c7c,basic,0.16016492864583332,0.14751720383693046
20211029,6a458c7c,basic-vars,0.19940283012820514,0.1572265697201018
20211029,6a458c7c,errors,0.08589596280642435,3.3116227956989253
115 changes: 69 additions & 46 deletions src/com/walmartlabs/lacinia/schema.clj
Original file line number Diff line number Diff line change
Expand Up @@ -447,10 +447,14 @@

(s/def ::apply-field-directives fn?)

(s/def ::disable-checks? boolean?)

(s/def ::compile-options (s/keys :opt-un [::default-field-resolver
::promote-nils-to-empty-list?
::enable-introspection?
::apply-field-directives]))
::apply-field-directives
::disable-checks?
::disable-java-objects?]))

(defn ^:private wrap-map
[compiled-schema m]
Expand Down Expand Up @@ -961,12 +965,15 @@
field-type-name - from the root :root kind "
[schema field-def field-type-name]
(let [field-type (or (get schema field-type-name)
(throw (ex-info (format "Field %s references unknown type %s."
(-> field-def :qualified-name q)
(-> field-def :type q))
{:field field-def
:schema-types (type-map schema)})))
(throw (ex-info (format "Field %s references unknown type %s."
(-> field-def :qualified-name q)
(-> field-def :type q))
{:field field-def
:schema-types (type-map schema)})))
category (:category field-type)
{:keys [disable-checks? disable-java-objects?]} (::options schema)

enable-java-objects? (not disable-java-objects?)

;; Build up layers of checks and other logic and a series of chained selector functions.
;; Normally, don't redefine local symbols, but here it makes it easier to follow and less
Expand All @@ -980,9 +987,6 @@
(fn select-coercion [execution-context selection callback path resolve-xf resolved-type resolved-value]
(cond-let

(nil? resolved-value)
(selector execution-context selection callback path resolve-xf resolved-type nil)

:let [serialized (try
(serializer resolved-value)
(catch Throwable t
Expand Down Expand Up @@ -1020,9 +1024,6 @@
;; and the enum's serializer converts that to a keyword, which is then
;; validated to match a known value for the enum.

(nil? resolved-value)
(selector execution-context selection callback path resolve-xf resolved-type nil)

:let [serialized (serializer resolved-value)]

(not (possible-values serialized))
Expand All @@ -1043,8 +1044,7 @@
(fn select-allowed-types [execution-context selection callback path resolve-xf resolved-type resolved-value]
(cond

(or (nil? resolved-value)
(contains? member-types resolved-type))
(contains? member-types resolved-type)
(selector execution-context selection callback path resolve-xf resolved-type resolved-value)

(nil? resolved-type)
Expand All @@ -1056,7 +1056,7 @@
(error "Value returned from resolver has incorrect type for field."
{:field-type field-type-name
:actual-type resolved-type
:allowed-types member-types}))))))
:allowed-types member-types}))))))


type-map (when union-or-interface?
Expand All @@ -1073,50 +1073,62 @@

;; This is needed because *sometimes* the same resolver is used for both a field
;; with an object type, and for a field with a union/interface type, and the value
;; may be a tagged value (wrapper around a Java object).
;; may be a tagged value (wrapper around a Java object). If :disable-java-objects? option is true,
;; then this step only applies to union or interface fields.

selector (fn select-unwrap-tagged-type [execution-context selection callback path resolve-xf resolved-type resolved-value]
(cond-let
;; Use explicitly tagged value (this usually applies to Java objects
;; that can't provide meta data).
(is-tagged-value? resolved-value)
(selector execution-context selection callback path resolve-xf (extract-type-tag resolved-value) (extract-value resolved-value))
selector (if-not (or union-or-interface? enable-java-objects?)
selector
(fn select-unwrap-tagged-type [execution-context selection callback path resolve-xf resolved-type resolved-value]
(cond-let
;; Use explicitly tagged value (this usually applies to Java objects
;; that can't provide meta data).
(is-tagged-value? resolved-value)
(selector execution-context selection callback path resolve-xf (extract-type-tag resolved-value) (extract-value resolved-value))

;; Check for explicit meta-data:
;; Check for explicit meta-data:

:let [type-name (-> resolved-value meta ::type-name)]
:let [type-name (-> resolved-value meta ::type-name)]

(some? type-name)
(selector execution-context selection callback path resolve-xf type-name resolved-value)
(some? type-name)
(selector execution-context selection callback path resolve-xf type-name resolved-value)

;; Use, if available, the mapping from tag to object that might be provided
;; for some objects.
:let [mapped-type (when type-map
(->> resolved-value
class
(get type-map)))]
;; Use, if available, the mapping from tag to object that might be provided
;; for some objects.
:let [mapped-type (when type-map
(->> resolved-value
class
(get type-map)))]

(some? mapped-type)
(selector execution-context selection callback path resolve-xf mapped-type resolved-value)
(some? mapped-type)
(selector execution-context selection callback path resolve-xf mapped-type resolved-value)

;; Let a later stage fail if it is a union or interface and there's no explicit
;; type.
:else
(selector execution-context selection callback path resolve-xf resolved-type resolved-value)))
;; Let a later stage fail if it is a union or interface and there's no explicit
;; type.
:else
(selector execution-context selection callback path resolve-xf resolved-type resolved-value))))


;; TODO: This could possibly be boosted up to the FieldSelection
selector (if-not (#{:object :input-object} category)
selector
(fn select-apply-static-type [execution-context selection callback path resolve-xf _resolved-type resolved-value]
;; TODO: Maybe a check that if the resolved value is tagged, that the tag matches the expected tag?
(selector execution-context selection callback path resolve-xf field-type-name resolved-value)))]
(selector execution-context selection callback path resolve-xf field-type-name resolved-value)))

(fn select-require-single-value [execution-context selection callback path resolve-xf resolved-type resolved-value]
(if (sequential-or-set? resolved-value)
(selector-error execution-context selection callback path resolve-xf
(error "Field resolver returned a collection of values, expected only a single value."))
(selector execution-context selection callback path resolve-xf resolved-type resolved-value)))))
selector (if disable-checks?
selector
(fn select-require-single-value [execution-context selection callback path resolve-xf resolved-type resolved-value]
(if (sequential-or-set? resolved-value)
(selector-error execution-context selection callback path resolve-xf
(error "Field resolver returned a collection of values, expected only a single value."))
(selector execution-context selection callback path resolve-xf resolved-type resolved-value))))

selector (if (= selector floor-selector)
selector
(fn select-bypass-if-nil [execution-context selection callback path resolve-xf resolved-type resolved-value]
(if (nil? resolved-value)
(callback execution-context path resolve-xf nil nil)
(selector execution-context selection callback path resolve-xf resolved-type resolved-value))))]
selector))

(defn ^:private assemble-selector
"Assembles a selector function for a field.
Expand Down Expand Up @@ -1860,7 +1872,6 @@
Compile options:
:default-field-resolver
: A function that accepts a field name (as a keyword) and converts it into the
default field resolver; this defaults to [[default-field-resolver]].
Expand All @@ -1873,6 +1884,18 @@
: If true (the default), then Schema introspection is enabled. Some applications
may disable introspection in production.
:disable-checks? (added in 1.1)
: If true (defaults to false), certain runtime checks on data returned from field resolvers
are omitted; this trades safety for speed, but may make sense when running in production.
:disable-java-objects? (added in 1.1)
: Normally, Lacinia must check each returned field to see if it is a wrapper around a Java object
(this happens when a Java objects is tagged via [[tag-with-type]]);
in most applications, resolvers return only Clojure values, not Java objects, and the step
that looks for tagged values iis only needed for fields
whose type is an interface or union. Using this option improves performance slightly, but should be
used consistently across environments (testing and production).
:apply-field-directives
: An optional callback function; for fields that have any directives on the field definition,
the callback is invoked; it is passed the [[FieldDef]] (from which directives may be extracted)
Expand Down

0 comments on commit cf2d178

Please sign in to comment.