~severeoverfl0w/slow-namespace-clj

5541c24e24cb95078d6b031202748ca3ba6a9ac3 — Dominic Monroe 1 year, 11 months ago
Add initial version
4 files changed, 142 insertions(+), 0 deletions(-)

A .gitignore
A README.md
A deps.edn
A src/io/dominic/slow_namespace_clj/core.clj
A  => .gitignore +1 -0
@@ 1,1 @@
.cpcache

A  => README.md +26 -0
@@ 1,26 @@
# slow-namespace-clj

Find which namespaces are slowest in your project.

## Usage

Add an alias to `~/.clojure/deps.edn`

```clojure
:user/slow-namespace-clj
{:extra-deps
 {io.dominic/slow-namespace-clj
  {:git/url "https://git.sr.ht/~severeoverfl0w/slow-namespace-clj"
   :sha "TODO"}}
 :main-opts ["-m" "io.dominic.slow-namespace-clj.core"]}
```

Then you can scan for just dependencies of your project, by specifying the
directories from your project

```shell
# Main project
$ clojure -A:user/slow-namespace-clj src
# For development
$ clojure -A:dev:user/slow-namespace-clj src dev test
```

A  => deps.edn +3 -0
@@ 1,3 @@
{:paths ["src"]
 :deps {org.clojure/tools.namespace {:mvn/version "1.0.0"}
        org.clojure/java.classpath {:mvn/version "1.0.0"}}}

A  => src/io/dominic/slow_namespace_clj/core.clj +112 -0
@@ 1,112 @@
(ns io.dominic.slow-namespace-clj.core
  (:require [clojure.tools.namespace.dependency :as dep]
            [clojure.tools.namespace.find :as find]
            [clojure.tools.namespace.parse :as parse]
            [clojure.java.classpath :as cp]
            [clojure.string :as string]))

(defn- recursive-ns-decls
  "Search scan-cp for ns decls, then recursively resolve all dependents using full classpath"
  [scan-cp]
  (let [scan-decls (find/find-ns-decls scan-cp find/clj)
        decl-idx (into {} (map (juxt parse/name-from-ns-decl identity)
                               (find/find-ns-decls (cp/classpath) find/clj)))]
    (loop [decls scan-decls
           final-decls []]
      (if (seq decls)
        (let [[decl & decls] decls]
          (recur
            (apply conj decls (map decl-idx (parse/deps-from-ns-decl decl)))
            (conj final-decls decl)))
        final-decls))))

(defn- ns-graph
  ([] (ns-graph (cp/classpath)))
  ([scan-cp]
   (reduce
     (fn [graph ns-decl]
       (reduce
         (fn [graph dep]
           (dep/depend graph (parse/name-from-ns-decl ns-decl) dep))
         graph
         (parse/deps-from-ns-decl ns-decl)))
     (dep/graph)
     (recursive-ns-decls scan-cp))))

(defmacro ^:private timed
  [expr]
  `(let [start# (. System (nanoTime))]
     ~expr
     (/ (double (- (. System (nanoTime)) start#)) 1000000.0) ))

(defn- prefixes
  [ns-string]
  (rest
    (take-while
      seq
      (iterate
        #(subs % 0 (or (string/last-index-of % ".") 0))
        ns-string)))) 

(def ^:private ^:dynamic *safe* false)

(defn- ns-timing
  [ns-graph]
  (let [timing (atom {})]
    (doseq [ns (dep/topo-sort ns-graph)]
      (binding [*out* *err*]
        (try
          (let [ttr (timed (if *safe*
                             (require ns :reload)
                             (require ns)))]
            (swap! timing assoc ns ttr))
          (catch clojure.lang.Compiler$CompilerException _
            (println "Exception while loading" ns))
          (catch ClassNotFoundException _
            (println "Exception while loading" ns))
          (catch java.io.FileNotFoundException _
            (println "Exception while loading" ns)))))
    @timing))

(defn- prefix-timing
  [timing]
  (let [ns-in-group (reduce
                      (fn [grouped ns]
                        (reduce
                          (fn [grouped prefix]
                            (update grouped prefix conj ns))
                          grouped
                          (prefixes (str ns))))
                      {}
                      (keys timing))
        ;; remove groups which are otherwise identical, pick the longest group.
        deduped-groups
        (vals (reduce-kv
                (fn [x group nss]
                  (update x nss (fn [old new] (max-key count (or old "") new)) group))
                {}
                ns-in-group))]
    (zipmap deduped-groups
            (map (fn [nss] (apply + (map #(get timing %) nss)))
                 (map ns-in-group deduped-groups)))))

(defn- run
  ([] (run {}))
  ([{:keys [threshold ns-graph]
     :or {threshold 0.1
          ns-graph (ns-graph)}}]
   (let [timing (ns-timing ns-graph)
         prefix-timing (prefix-timing timing)]
     (doseq [[ns ttr] (sort-by val timing)
             :when (> ttr threshold)]
       (println ns ":" ttr "msecs"))
     (println "-- [Groups] --")
     (doseq [[group ttr] (sort-by key prefix-timing)]
       (println group ":" ttr "msecs")))))

(defn -main
  [& dirs]
  (binding [*safe* true]
    (if (seq dirs)
      (run {:ns-graph (ns-graph (map #(java.io.File. %) dirs))})
      (run))))