Skip to content

Speed up reflike? on the JVM - #117

Open
mpenttila wants to merge 1 commit into
weavejester:masterfrom
cloudpermit:faster-reflike
Open

mpenttila wants to merge 1 commit into
weavejester:masterfrom
cloudpermit:faster-reflike

Conversation

@mpenttila

Copy link
Copy Markdown

reflike? is called on every value in a configuration, several times per init (dependency graph, ref
validation, ref resolution). On the JVM, satisfies? is slow when the answer is false, as it searches the class
hierarchy of the value for a protocol implementation and does not cache misses. Most values in a large configuration
are not refs, so init time can get long.

This changes reflike? to check for the protocol's interface with instance? on the JVM. Ref and RefSet
implement RefLike inline, so they are still recognized. ClojureScript keeps satisfies?, which is fast there.
Babashka also supports the interface check.

There is one behaviour difference: a type that implements RefLike through extend-protocol or extend-type would no longer be treated as a ref on the JVM. I don't know if anyone would be doing that.

In our main app Integrant goes through 400k+ nodes, so this has a meaningful impact on our app's startup time.

Benchmark

A configuration of 100 components whose values are plain nested data (~117k nodes in total), each with a ref to the
previous component:

;; lein run -m clojure.main bench.clj
(require '[integrant.core :as ig])

(defmethod ig/init-key ::component [_ v] v)

(defn- component-data [i]
  {:name     (str "component-" i)
   :settings (into {} (for [j (range 50)] [(keyword (str "setting-" j)) {:value j, :enabled? (even? j)}]))
   :routes   (vec (for [j (range 50)] {:path (str "/api/" i "/" j), :methods #{:get :post}, :roles [:admin :user]}))})

(def config
  (into {}
        (for [i (range 100)]
          (let [k (keyword "bench" (str "component-" i))]
            (derive k ::component)
            [k (cond-> (component-data i)
                 (pos? i) (assoc :previous (ig/ref (keyword "bench" (str "component-" (dec i))))))]))))

(defn- time-ms [f]
  (let [start (System/nanoTime)]
    (f)
    (/ (- (System/nanoTime) start) 1e6)))

(println "Config nodes:" (count (tree-seq coll? seq config)))
(dotimes [_ 3] (ig/halt! (ig/init config)))
(let [times (sort (repeatedly 10 #(time-ms (fn [] (ig/halt! (ig/init config))))))]
  (println (format "ig/init + ig/halt! median: %.0f ms" (nth times 5))))

Median of 10 runs of ig/init + ig/halt! (Clojure 1.11.4, JDK 25, M4):

Median
master 4040 ms
this branch 881 ms

Integrant calls reflike? on every value in a configuration, several
times per init. On the JVM, satisfies? is slow for values that do not
implement the protocol, which can be nearly all of them, so init time
can grow noticeably with the size of the configuration. Check for the
protocol interface instead, which the Ref and RefSet records implement.
@NoahTheDuke

Copy link
Copy Markdown

an alternative approach could be to change reflike? to a method on the protocol, and implement defaults to check satisfies? and then implementors can implement it to be fast.

@weavejester

Copy link
Copy Markdown
Owner

On the one hand this could break any existing application that uses a custom reflike that's created via extend-protocol, but on the other hand there's nothing to suggest this was a public protocol intended for that use, and this speedup looks useful.

The reflike? function itself has the docstring "Return true if its argument is a ref or a refset.". This PR wouldn't change the documented functionality. Similarly, while the methods of the Reflike protocol have a docstring, the protocol itself notably doesn't.

It also seems unlikely to me that anyone would implement their own Reflike and not have it be a record. There isn't any advantage I can think of to not checking the instance.

So I think I'm in favour of this change, after giving it a little thought. However, I'll leave this PR open for a little while to give folk time to object.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants