diff --git a/README.md b/README.md index be73d1b..4769e9c 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,31 @@ user> (-> (->bounds 0 0 800 600) user> ``` +Oh, and quadtree-cljc is fast: + +``` clojure +;; Benched with criterium on JDK21/M2 Max MBP +user> (let [entities (generate-entities ->random-large-entity 1000) + player (->random-small-entity)] + (bench/bench (-> (->bounds 0 0 1920 1080) + (->quadtree 1020 100) + (insert-all entities) + (retrieve-intersections player)))) + +Evaluation count : 155640 in 60 samples of 2594 calls. +Execution time mean : 389.595233 µs +Execution time std-deviation : 2.873750 µs +Execution time lower quantile : 383.609006 µs ( 2.5%) +Execution time upper quantile : 393.246808 µs (97.5%) +Overhead used : 1.666329 ns + +Found 1 outliers in 60 samples (1.6667 %) +low-severe 1 (1.6667 %) +Variance from outliers : 1.6389 % Variance is slightly inflated by outliers +nil +``` + + If you were using this in a game, you might build up your quadtree on every game frame, inserting all your entities bounds, and then decide if you should update your entities. diff --git a/deps.edn b/deps.edn index 9c853c7..ab5c2cb 100644 --- a/deps.edn +++ b/deps.edn @@ -1,5 +1,4 @@ -{:paths ["src" - "test"] +{:paths ["src"] :deps {org.clojure/clojure {:mvn/version "1.11.1"} org.clojure/clojurescript {:mvn/version "1.10.764"}} :aliases {:build {:deps {slipset/deps-deploy {:mvn/version "0.2.0"} diff --git a/test/quadtree_cljc/core_test.cljc b/test/quadtree_cljc/core_test.cljc index 024aafe..fc56fb1 100644 --- a/test/quadtree_cljc/core_test.cljc +++ b/test/quadtree_cljc/core_test.cljc @@ -11,7 +11,9 @@ retrieve-points]] #?@(:clj [[clojure.test :refer [deftest is testing run-tests]] [criterium.core :as bench]] - :cljs [[cljs.test :refer-macros [deftest is testing run-tests]]]))) + :cljs [[cljs.test :refer-macros [deftest is testing run-tests]] + [quadtree-cljc.macros :refer-macros [benchmark]] + [cljs.core :refer-macros [simple-benchmark]]]))) (deftest total-nodes-test (testing "Correct use case for total-nodes" @@ -84,272 +86,272 @@ :objects [], :nodes []}]} (split quadtree))))) - (testing "Incorrect use case for split" - (let [one-node {:bounds {:x 0, :y 0, :width 800, :height 600}, - :max-objects 10, - :max-levels 10, - :level 0, - :objects [], - :nodes - [{:bounds {:x 400, :y 0, :width 400, :height 300}, + #?(:clj (testing "Incorrect use case for split" + (let [one-node {:bounds {:x 0, :y 0, :width 800, :height 600}, + :max-objects 10, + :max-levels 10, + :level 0, + :objects [], + :nodes + [{:bounds {:x 400, :y 0, :width 400, :height 300}, + :max-objects 10, + :max-levels 10, + :level 1, + :objects [], + :nodes []}]} + no-nodes {:bounds {:x 0, :y 0, :width 800, :height 600}, + :max-objects 10, + :max-levels 10, + :level 0, + :objects []}] + (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, :max-objects 10, :max-levels 10, - :level 1, + :level 0, :objects [], - :nodes []}]} - no-nodes {:bounds {:x 0, :y 0, :width 800, :height 600}, - :max-objects 10, - :max-levels 10, - :level 0, - :objects []}] - (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, - :max-objects 10, - :max-levels 10, - :level 0, - :objects [], - :nodes - [{:bounds {:x 400, :y 0, :width 400, :height 300}, - :max-objects 10, - :max-levels 10, - :level 1, - :objects [], - :nodes []} - {:bounds {:x 0, :y 0, :width 400, :height 300}, - :max-objects 10, - :max-levels 10, - :level 1, - :objects [], - :nodes []} - {:bounds {:x 0, :y 300, :width 400, :height 300}, - :max-objects 10, - :max-levels 10, - :level 1, - :objects [], - :nodes []} - {:bounds {:x 400, :y 300, :width 400, :height 300}, - :max-objects 10, - :max-levels 10, - :level 1, - :objects [], - :nodes []}]} (split one-node))) - (is (thrown? NullPointerException (split []))) - (is (thrown? NullPointerException (split {}))) - (is (thrown? NullPointerException (split 231321654)))))) + :nodes + [{:bounds {:x 400, :y 0, :width 400, :height 300}, + :max-objects 10, + :max-levels 10, + :level 1, + :objects [], + :nodes []} + {:bounds {:x 0, :y 0, :width 400, :height 300}, + :max-objects 10, + :max-levels 10, + :level 1, + :objects [], + :nodes []} + {:bounds {:x 0, :y 300, :width 400, :height 300}, + :max-objects 10, + :max-levels 10, + :level 1, + :objects [], + :nodes []} + {:bounds {:x 400, :y 300, :width 400, :height 300}, + :max-objects 10, + :max-levels 10, + :level 1, + :objects [], + :nodes []}]} (split one-node))) + (is (thrown? NullPointerException (split []))) + (is (thrown? NullPointerException (split {}))) + (is (thrown? NullPointerException (split 231321654))))))) -(deftest insert-tests - (testing "insert via insert-all" - (let [basic-quadtree (-> (->bounds 0 0 800 600) - (->quadtree 10 10 0 [] [])) - medium-quadtree (-> (->bounds 0 0 800 600) - (->quadtree 5 5 0 [] [])) - max-levels-quadtree (-> (->bounds 0 0 800 600) - (->quadtree 1 10 0 [] []))] - (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, - :max-objects 5, - :max-levels 5, - :level 0, - :objects [], - :nodes - [{:bounds {:x 400, :y 0, :width 400, :height 300}, - :max-objects 5, - :max-levels 5, - :level 1, - :objects [], - :nodes []} - {:bounds {:x 0, :y 0, :width 400, :height 300}, - :max-objects 5, - :max-levels 5, - :level 1, - :objects - [{:x 0, :y 0, :width 10, :height 10} - {:x 0, :y 5, :width 10, :height 10} - {:x 100, :y 150, :width 10, :height 10} - {:x 110, :y 160, :width 10, :height 10}], - :nodes []} - {:bounds {:x 0, :y 300, :width 400, :height 300}, - :max-objects 5, - :max-levels 5, - :level 1, - :objects - [{:x 160, :y 390, :width 10, :height 10} - {:x 160, :y 400, :width 10, :height 10}], - :nodes []} - {:bounds {:x 400, :y 300, :width 400, :height 300}, - :max-objects 5, - :max-levels 5, - :level 1, - :objects [], - :nodes []}]} (insert-all medium-quadtree [{:x 0 :y 0 :width 10 :height 10} - {:x 0 :y 5 :width 10 :height 10} - {:x 100 :y 150 :width 10 :height 10} - {:x 110 :y 160 :width 10 :height 10} - {:x 160 :y 390 :width 10 :height 10} - {:x 160 :y 400 :width 10 :height 10}])) - (= {:bounds {:x 0, :y 0, :width 800, :height 600}, - :max-objects 1, - :max-levels 10, - :level 0, - :objects [], - :nodes - [{:bounds {:x 400, :y 0, :width 400, :height 300}, - :max-objects 1, - :max-levels 10, - :level 1, - :objects [], - :nodes []} - {:bounds {:x 0, :y 0, :width 400, :height 300}, - :max-objects 1, - :max-levels 10, - :level 1, - :objects [], - :nodes - [{:bounds {:x 200, :y 0, :width 200, :height 150}, - :max-objects 1, - :max-levels 10, - :level 2, - :objects [], - :nodes []} - {:bounds {:x 0, :y 0, :width 200, :height 150}, - :max-objects 1, - :max-levels 10, - :level 2, - :objects [], - :nodes - [{:bounds {:x 100, :y 0, :width 100, :height 75}, - :max-objects 1, - :max-levels 10, - :level 3, - :objects [], - :nodes []} - {:bounds {:x 0, :y 0, :width 100, :height 75}, - :max-objects 1, - :max-levels 10, - :level 3, - :objects [], - :nodes - [{:bounds {:x 50, :y 0, :width 50, :height 75/2}, - :max-objects 1, - :max-levels 10, - :level 4, +#?(:clj (deftest insert-tests + (testing "insert via insert-all" + (let [basic-quadtree (-> (->bounds 0 0 800 600) + (->quadtree 10 10 0 [] [])) + medium-quadtree (-> (->bounds 0 0 800 600) + (->quadtree 5 5 0 [] [])) + max-levels-quadtree (-> (->bounds 0 0 800 600) + (->quadtree 1 10 0 [] []))] + (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, + :max-objects 5, + :max-levels 5, + :level 0, :objects [], - :nodes []} - {:bounds {:x 0, :y 0, :width 50, :height 75/2}, + :nodes + [{:bounds {:x 400, :y 0, :width 400, :height 300}, + :max-objects 5, + :max-levels 5, + :level 1, + :objects [], + :nodes []} + {:bounds {:x 0, :y 0, :width 400, :height 300}, + :max-objects 5, + :max-levels 5, + :level 1, + :objects + [{:x 0, :y 0, :width 10, :height 10} + {:x 0, :y 5, :width 10, :height 10} + {:x 100, :y 150, :width 10, :height 10} + {:x 110, :y 160, :width 10, :height 10}], + :nodes []} + {:bounds {:x 0, :y 300, :width 400, :height 300}, + :max-objects 5, + :max-levels 5, + :level 1, + :objects + [{:x 160, :y 390, :width 10, :height 10} + {:x 160, :y 400, :width 10, :height 10}], + :nodes []} + {:bounds {:x 400, :y 300, :width 400, :height 300}, + :max-objects 5, + :max-levels 5, + :level 1, + :objects [], + :nodes []}]} (insert-all medium-quadtree [{:x 0 :y 0 :width 10 :height 10} + {:x 0 :y 5 :width 10 :height 10} + {:x 100 :y 150 :width 10 :height 10} + {:x 110 :y 160 :width 10 :height 10} + {:x 160 :y 390 :width 10 :height 10} + {:x 160 :y 400 :width 10 :height 10}])) + (= {:bounds {:x 0, :y 0, :width 800, :height 600}, :max-objects 1, :max-levels 10, - :level 4, + :level 0, :objects [], :nodes - [{:bounds {:x 25, :y 0, :width 25, :height 75/4}, + [{:bounds {:x 400, :y 0, :width 400, :height 300}, :max-objects 1, :max-levels 10, - :level 5, + :level 1, :objects [], :nodes []} - {:bounds {:x 0, :y 0, :width 25, :height 75/4}, + {:bounds {:x 0, :y 0, :width 400, :height 300}, :max-objects 1, :max-levels 10, - :level 5, - :objects - [{:x 0, :y 0, :width 10, :height 10} - {:x 0, :y 5, :width 10, :height 10}], + :level 1, + :objects [], :nodes - [{:bounds {:x 25/2, :y 0, :width 25/2, :height 75/8}, + [{:bounds {:x 200, :y 0, :width 200, :height 150}, :max-objects 1, :max-levels 10, - :level 6, + :level 2, :objects [], :nodes []} - {:bounds {:x 0, :y 0, :width 25/2, :height 75/8}, + {:bounds {:x 0, :y 0, :width 200, :height 150}, :max-objects 1, :max-levels 10, - :level 6, + :level 2, :objects [], - :nodes []} - {:bounds {:x 0, :y 75/8, :width 25/2, :height 75/8}, + :nodes + [{:bounds {:x 100, :y 0, :width 100, :height 75}, + :max-objects 1, + :max-levels 10, + :level 3, + :objects [], + :nodes []} + {:bounds {:x 0, :y 0, :width 100, :height 75}, + :max-objects 1, + :max-levels 10, + :level 3, + :objects [], + :nodes + [{:bounds {:x 50, :y 0, :width 50, :height 75/2}, + :max-objects 1, + :max-levels 10, + :level 4, + :objects [], + :nodes []} + {:bounds {:x 0, :y 0, :width 50, :height 75/2}, + :max-objects 1, + :max-levels 10, + :level 4, + :objects [], + :nodes + [{:bounds {:x 25, :y 0, :width 25, :height 75/4}, + :max-objects 1, + :max-levels 10, + :level 5, + :objects [], + :nodes []} + {:bounds {:x 0, :y 0, :width 25, :height 75/4}, + :max-objects 1, + :max-levels 10, + :level 5, + :objects + [{:x 0, :y 0, :width 10, :height 10} + {:x 0, :y 5, :width 10, :height 10}], + :nodes + [{:bounds {:x 25/2, :y 0, :width 25/2, :height 75/8}, + :max-objects 1, + :max-levels 10, + :level 6, + :objects [], + :nodes []} + {:bounds {:x 0, :y 0, :width 25/2, :height 75/8}, + :max-objects 1, + :max-levels 10, + :level 6, + :objects [], + :nodes []} + {:bounds {:x 0, :y 75/8, :width 25/2, :height 75/8}, + :max-objects 1, + :max-levels 10, + :level 6, + :objects [], + :nodes []} + {:bounds {:x 25/2, :y 75/8, :width 25/2, :height 75/8}, + :max-objects 1, + :max-levels 10, + :level 6, + :objects [], + :nodes []}]} + {:bounds {:x 0, :y 75/4, :width 25, :height 75/4}, + :max-objects 1, + :max-levels 10, + :level 5, + :objects [], + :nodes []} + {:bounds {:x 25, :y 75/4, :width 25, :height 75/4}, + :max-objects 1, + :max-levels 10, + :level 5, + :objects [], + :nodes []}]} + {:bounds {:x 0, :y 75/2, :width 50, :height 75/2}, + :max-objects 1, + :max-levels 10, + :level 4, + :objects [], + :nodes []} + {:bounds {:x 50, :y 75/2, :width 50, :height 75/2}, + :max-objects 1, + :max-levels 10, + :level 4, + :objects [], + :nodes []}]} + {:bounds {:x 0, :y 75, :width 100, :height 75}, + :max-objects 1, + :max-levels 10, + :level 3, + :objects [], + :nodes []} + {:bounds {:x 100, :y 75, :width 100, :height 75}, + :max-objects 1, + :max-levels 10, + :level 3, + :objects [], + :nodes []}]} + {:bounds {:x 0, :y 150, :width 200, :height 150}, :max-objects 1, :max-levels 10, - :level 6, + :level 2, :objects [], :nodes []} - {:bounds {:x 25/2, :y 75/8, :width 25/2, :height 75/8}, + {:bounds {:x 200, :y 150, :width 200, :height 150}, :max-objects 1, :max-levels 10, - :level 6, + :level 2, :objects [], :nodes []}]} - {:bounds {:x 0, :y 75/4, :width 25, :height 75/4}, + {:bounds {:x 0, :y 300, :width 400, :height 300}, :max-objects 1, :max-levels 10, - :level 5, + :level 1, :objects [], :nodes []} - {:bounds {:x 25, :y 75/4, :width 25, :height 75/4}, + {:bounds {:x 400, :y 300, :width 400, :height 300}, :max-objects 1, :max-levels 10, - :level 5, + :level 1, :objects [], - :nodes []}]} - {:bounds {:x 0, :y 75/2, :width 50, :height 75/2}, - :max-objects 1, + :nodes []}]} (insert-all max-levels-quadtree [{:x 0 :y 0 :width 10 :height 10} + {:x 0 :y 5 :width 10 :height 10}]))) + (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, + :max-objects 10, :max-levels 10, - :level 4, - :objects [], - :nodes []} - {:bounds {:x 50, :y 75/2, :width 50, :height 75/2}, - :max-objects 1, + :level 0, + :objects [{:x 0, :y 0, :width 10, :height 10}], + :nodes []} (insert basic-quadtree {:x 0 :y 0 :width 10 :height 10}))) + (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, + :max-objects 10, :max-levels 10, - :level 4, + :level 0, :objects [], - :nodes []}]} - {:bounds {:x 0, :y 75, :width 100, :height 75}, - :max-objects 1, - :max-levels 10, - :level 3, - :objects [], - :nodes []} - {:bounds {:x 100, :y 75, :width 100, :height 75}, - :max-objects 1, - :max-levels 10, - :level 3, - :objects [], - :nodes []}]} - {:bounds {:x 0, :y 150, :width 200, :height 150}, - :max-objects 1, - :max-levels 10, - :level 2, - :objects [], - :nodes []} - {:bounds {:x 200, :y 150, :width 200, :height 150}, - :max-objects 1, - :max-levels 10, - :level 2, - :objects [], - :nodes []}]} - {:bounds {:x 0, :y 300, :width 400, :height 300}, - :max-objects 1, - :max-levels 10, - :level 1, - :objects [], - :nodes []} - {:bounds {:x 400, :y 300, :width 400, :height 300}, - :max-objects 1, - :max-levels 10, - :level 1, - :objects [], - :nodes []}]} (insert-all max-levels-quadtree [{:x 0 :y 0 :width 10 :height 10} - {:x 0 :y 5 :width 10 :height 10}]))) - (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, - :max-objects 10, - :max-levels 10, - :level 0, - :objects [{:x 0, :y 0, :width 10, :height 10}], - :nodes []} (insert basic-quadtree {:x 0 :y 0 :width 10 :height 10}))) - (is (= {:bounds {:x 0, :y 0, :width 800, :height 600}, - :max-objects 10, - :max-levels 10, - :level 0, - :objects [], - :nodes []} (insert-all basic-quadtree [])))))) + :nodes []} (insert-all basic-quadtree []))))))) (defn- ->random-small-entity "Simulates a small entity in a 1080p world" diff --git a/test/quadtree_cljc/macros.cljc b/test/quadtree_cljc/macros.cljc new file mode 100644 index 0000000..af6293c --- /dev/null +++ b/test/quadtree_cljc/macros.cljc @@ -0,0 +1,21 @@ +(ns quadtree-cljc.macros + (:require #?(:clj [clojure.test :as t] + :cljs [cljs.test :as t :include-macros true]))) + + +(defn get-timestamp + [] + #?(:clj (System/currentTimeMillis) + :cljs (js/performance.now))) + +(defmacro benchmark + [bindings expr iterations & {:keys [print-fn] :or {print-fn 'println}}] + (let [bs-str (pr-str bindings) + expr-str (pr-str expr)] + `(let ~bindings + (let [start# (get-timestamp) + ret# (dotimes [_# ~iterations] ~expr) + end# (get-timestamp) + elapsed# (- end# start#)] + (~print-fn (str ~bs-str ", " ~expr-str ", " + ~iterations " runs, " elapsed# " msecs"))))))