From fc9cca91fb6f4e9fe3bf4e64c4a49f5d47af7542 Mon Sep 17 00:00:00 2001 From: Jamie English Date: Sat, 4 Apr 2026 20:42:02 +0100 Subject: [PATCH] perf(gen_build): skip gen-dir when BUILD.bazel is newer than sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an mtime check so directories whose BUILD.bazel is already newer than all source files are skipped entirely. Adds ~2s overhead on cold runs (checking mtimes with no BUILD files present) but reduces incremental (warm) runs from ~10s to ~1.8s. ### Benchmark `examples/gen_srcs_bench` (Hyperfine, 5 runs, 19607 namespaces) - cold (prepare: delete BUILD.bazel): 9.660s → 11.696s ± 1.943s - warm (no-op, all BUILD files current): **1.841s ± 0.090s** Co-Authored-By: Claude Opus 4.6 (1M context) --- src/rules_clojure/gen_build.clj | 105 ++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/src/rules_clojure/gen_build.clj b/src/rules_clojure/gen_build.clj index 3ff8a46..da163ff 100644 --- a/src/rules_clojure/gen_build.clj +++ b/src/rules_clojure/gen_build.clj @@ -660,60 +660,71 @@ "given a source directory, write a BUILD.bazel for all .clj files in the directory. non-recursive" [{:keys [deps-edn-dir] :as args} dir] (assert (map? (:src-ns->label args))) - (let [paths (->> (fs/ls dir) + (let [entries (fs/ls dir) + paths (->> entries (filter (fn [p] (or (clj*-path? p) (js-path? p)))) (sort-by str)) - subdirs (->> dir - fs/ls + build-file (-> dir (fs/->path "BUILD.bazel") fs/path->file) + build-mtime (when (.exists build-file) (.lastModified build-file)) + subdirs (->> entries (filter (fn [^Path p] (-> p .toFile fs/directory?))) (sort-by str)) - clj-subdirs (->> - subdirs - (filter (fn [p] - (some clj*-path? (seq (fs/ls-r p)))))) - [has-binary? rules] (reduce (fn [[hb? acc] rule] - [(or hb? (= :clojure_binary (:type rule))) - (conj acc (emit-rule rule))]) - [false []] - (->> paths - (group-by fs/basename) - (mapcat (fn [[_base paths]] - (ns-rules args paths))))) - - content (str "#autogenerated, do not edit\n" - (emit-bazel (list 'package (kwargs {:default_visibility ["//visibility:public"]}))) - "\n" - (emit-bazel (apply list 'load "@rules_clojure//:rules.bzl" - (cond-> ["clojure_library" "clojure_test"] - has-binary? (conj "clojure_binary")))) - "\n" - "\n" - (str/join "\n\n" rules) - "\n" - "\n" - (when (or (seq paths) (seq clj-subdirs)) - (str - (emit-bazel (list 'clojure_library (kwargs {:name "__clj_lib" - :deps (vec - (concat - (dedupe (map (fn [p] (str ":" (fs/basename p))) paths)) - (map (fn [p] - (str "//" (fs/path-relative-to deps-edn-dir p) ":__clj_lib")) clj-subdirs)))}))) - "\n" - "\n" - (emit-bazel (list 'filegroup (kwargs (merge - {:name "__clj_files" - :srcs (mapv fs/filename paths) - :data (mapv (fn [p] - (str "//" (fs/path-relative-to deps-edn-dir p) ":__clj_files")) clj-subdirs)})))))))] - (let [build-file (-> dir (fs/->path "BUILD.bazel") fs/path->file) - changed? (not= content (when (.exists build-file) (slurp build-file :encoding "UTF-8")))] - (when changed? - (spit build-file content :encoding "UTF-8")) - {:files (count paths) :wrote (if changed? 1 0)}))) + skip? (and build-mtime + (every? (fn [^Path p] + (<= (.lastModified (.toFile p)) ^long build-mtime)) + paths) + (every? (fn [^Path p] + (let [child-build (-> p (fs/->path "BUILD.bazel") fs/path->file)] + (or (not (.exists child-build)) + (<= (.lastModified child-build) ^long build-mtime)))) + subdirs))] + (if skip? + {:files (count paths) :wrote 0} + (let [clj-subdirs (->> + subdirs + (filter (fn [p] + (some clj*-path? (seq (fs/ls-r p)))))) + [has-binary? rules] (reduce (fn [[hb? acc] rule] + [(or hb? (= :clojure_binary (:type rule))) + (conj acc (emit-rule rule))]) + [false []] + (->> paths + (group-by fs/basename) + (mapcat (fn [[_base paths]] + (ns-rules args paths))))) + content (str "#autogenerated, do not edit\n" + (emit-bazel (list 'package (kwargs {:default_visibility ["//visibility:public"]}))) + "\n" + (emit-bazel (apply list 'load "@rules_clojure//:rules.bzl" + (cond-> ["clojure_library" "clojure_test"] + has-binary? (conj "clojure_binary")))) + "\n" + "\n" + (str/join "\n\n" rules) + "\n" + "\n" + (when (or (seq paths) (seq clj-subdirs)) + (str + (emit-bazel (list 'clojure_library (kwargs {:name "__clj_lib" + :deps (vec + (concat + (dedupe (map (fn [p] (str ":" (fs/basename p))) paths)) + (map (fn [p] + (str "//" (fs/path-relative-to deps-edn-dir p) ":__clj_lib")) clj-subdirs)))}))) + "\n" + "\n" + (emit-bazel (list 'filegroup (kwargs (merge + {:name "__clj_files" + :srcs (mapv fs/filename paths) + :data (mapv (fn [p] + (str "//" (fs/path-relative-to deps-edn-dir p) ":__clj_files")) clj-subdirs)}))))))) + changed? (not= content (when build-mtime (slurp build-file :encoding "UTF-8")))] + (when changed? + (spit build-file content :encoding "UTF-8")) + {:files (count paths) :wrote (if changed? 1 0)})))) (s/fdef gen-source-paths- :args (s/cat :a (s/keys :req-un [::deps-edn-dir ::src-ns->label ::dep-ns->label ::jar->lib ::deps-repo-tag ::deps-bazel]) :paths (s/coll-of fs/path?))) (defn gen-source-paths-