Fast startup: inference is opt-in, and dependencies compile once (jolt-87e)
Two things made a program run catastrophically slow to start (ring-app: 111s), which is backwards — direct-linking should make startup FASTER (Clojure: smaller classes, faster startup; Stalin: whole-program analysis is strictly ahead-of- time, zero startup cost). We were doing the opposite: paying compile-time work at every startup. 1. Decouple inference from direct-linking. Direct-linking (cheap compile-time call resolution) was bundled with the whole inference/specialization pipeline (inlining + scalar replacement + structural types + the per-ns re-emit fixpoint) — the expensive part. Now a program run direct-links + uses records by default (fast, faster calls) but the inference is opt-in via JOLT_OPTIMIZE (or an explicit JOLT_DIRECT_LINK / the native build). ring-app: 111s -> 6s. 2. Compile dependencies once. The baked core loads in ~10ms, but a program still re-compiled every dependency namespace (reitit, ring, selmer, honeysql, …) from source on every run — the whole 6s. Snapshot the ctx after the require chain to a per-project image (the same marshal/fork the core image uses), keyed on version + entry + source roots + flags, with a per-file mtime manifest so any edit invalidates it. First run compiles + caches; later runs fork the image (~10ms) and skip compilation. ring-app: 6s -> ~1s warm, competitive with the JVM. JOLT_NO_DEPS_CACHE disables. Full gate green (all -m integration tests now exercise the cache); deps-image invalidation verified (touching a source recompiles).
This commit is contained in:
parent
cf271e218a
commit
9f1cebc097
1 changed files with 82 additions and 11 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
(use ./plist)
|
(use ./plist)
|
||||||
(use ./config)
|
(use ./config)
|
||||||
(use ./reader)
|
(use ./reader)
|
||||||
|
(import ./core :as jcore)
|
||||||
|
|
||||||
(def jolt-version "0.1.0")
|
(def jolt-version "0.1.0")
|
||||||
|
|
||||||
|
|
@ -17,7 +18,9 @@
|
||||||
# result always matches the interpreter; see backend.janet / loader/eval-toplevel).
|
# result always matches the interpreter; see backend.janet / loader/eval-toplevel).
|
||||||
# Set JOLT_INTERPRET=1 to force the tree-walking interpreter (debugging / A-B).
|
# Set JOLT_INTERPRET=1 to force the tree-walking interpreter (debugging / A-B).
|
||||||
(def compile-default? (not (= "1" (os/getenv "JOLT_INTERPRET"))))
|
(def compile-default? (not (= "1" (os/getenv "JOLT_INTERPRET"))))
|
||||||
(def ctx (init {:compile? compile-default?}))
|
# A var, not a def: a -m run may replace it with a forked deps-image ctx that
|
||||||
|
# already has every dependency compiled (see run-main / the deps image cache).
|
||||||
|
(var ctx (init {:compile? compile-default?}))
|
||||||
(ctx-set-current-ns ctx "user")
|
(ctx-set-current-ns ctx "user")
|
||||||
|
|
||||||
(defn read-line [prompt]
|
(defn read-line [prompt]
|
||||||
|
|
@ -424,19 +427,77 @@
|
||||||
(defn- print-version []
|
(defn- print-version []
|
||||||
(print "jolt v" jolt-version))
|
(print "jolt v" jolt-version))
|
||||||
|
|
||||||
|
# --- Deps image cache: compile the dependencies ONCE -------------------------
|
||||||
|
# The baked binary loads core in ~10ms (it's marshaled in), but a program still
|
||||||
|
# re-compiles ALL of its dependency namespaces (reitit, ring, …) from source on
|
||||||
|
# every run — seconds of analyzer->IR->emit that never change between runs. Like
|
||||||
|
# Clojure's AOT or Stalin's compile-once model, snapshot the ctx AFTER the
|
||||||
|
# require chain and reuse it: the first run compiles + caches, later runs fork
|
||||||
|
# the image (~10ms) and skip compilation entirely. The key includes the jolt
|
||||||
|
# version, entry ns, source roots, and the compile flags; the image carries a
|
||||||
|
# manifest of every loaded source file's mtime, so any source edit (app or dep)
|
||||||
|
# invalidates it. JOLT_NO_DEPS_CACHE=1 disables.
|
||||||
|
(defn- deps-image-path [ns-name]
|
||||||
|
(def dir (or (os/getenv "JOLT_IMAGE_CACHE_DIR") (os/getenv "TMPDIR") "/tmp"))
|
||||||
|
(def env (ctx :env))
|
||||||
|
(def key (string/format "%q|%q|%q|%q|%q|%q|%q|%q"
|
||||||
|
jolt-version ns-name (os/getenv "JOLT_PATH")
|
||||||
|
(get env :direct-linking?) (get env :inline?)
|
||||||
|
(get env :shapes?) (get env :whole-program?)
|
||||||
|
(os/getenv "JOLT_FEATURES")))
|
||||||
|
(string dir "/jolt-deps-" (band (hash key) 0x7FFFFFFF) ".jimg"))
|
||||||
|
|
||||||
|
(defn- manifest-of [files]
|
||||||
|
(def m @{})
|
||||||
|
(each f files (when-let [st (os/stat f)] (put m f (st :modified))))
|
||||||
|
m)
|
||||||
|
|
||||||
|
(defn- manifest-current? [manifest]
|
||||||
|
(var ok true)
|
||||||
|
(eachp [f mt] manifest
|
||||||
|
(def st (os/stat f))
|
||||||
|
(unless (and st (= (st :modified) mt)) (set ok false)))
|
||||||
|
ok)
|
||||||
|
|
||||||
|
(defn- try-load-deps-image [path]
|
||||||
|
(when (and (not (os/getenv "JOLT_NO_DEPS_CACHE")) (os/stat path))
|
||||||
|
(def r (protect (fork (slurp path))))
|
||||||
|
(when (and (r 0) (ctx? (r 1)))
|
||||||
|
(def c (r 1))
|
||||||
|
(def manifest (get (c :env) :deps-manifest))
|
||||||
|
(when (and manifest (manifest-current? manifest))
|
||||||
|
# per-process wiring an image restore must redo (module state, not in the
|
||||||
|
# marshaled ctx) — same as init-cached does for the core image.
|
||||||
|
(jcore/install-print-method-cb! c)
|
||||||
|
c))))
|
||||||
|
|
||||||
|
(defn- save-deps-image [c path]
|
||||||
|
(unless (os/getenv "JOLT_NO_DEPS_CACHE")
|
||||||
|
(put (c :env) :deps-manifest (manifest-of (or (get (c :env) :loaded-files) @[])))
|
||||||
|
(def tmp (string path "." (os/getpid) ".tmp"))
|
||||||
|
(when (protect (spit tmp (snapshot c)))
|
||||||
|
(protect (os/rename tmp path)))))
|
||||||
|
|
||||||
(defn- run-main [ns-name argv]
|
(defn- run-main [ns-name argv]
|
||||||
(when (nil? ns-name) (eprint "Error: -m/--main requires a namespace") (os/exit 1))
|
(when (nil? ns-name) (eprint "Error: -m/--main requires a namespace") (os/exit 1))
|
||||||
(set-command-line-args argv)
|
(set-command-line-args argv)
|
||||||
(try
|
(try
|
||||||
(do
|
(do
|
||||||
|
(def path (deps-image-path ns-name))
|
||||||
|
(if-let [cached (try-load-deps-image path)]
|
||||||
|
# cache hit: every dependency is already compiled in the image
|
||||||
|
(do (set ctx cached) (ctx-set-current-ns ctx "user"))
|
||||||
|
# cache miss: compile the requires, then snapshot for next time. Track the
|
||||||
|
# loaded files for the manifest.
|
||||||
|
(do
|
||||||
|
(put (ctx :env) :loaded-files @[])
|
||||||
(load-string ctx (string "(require '[" ns-name "])"))
|
(load-string ctx (string "(require '[" ns-name "])"))
|
||||||
# whole-program (jolt-t34): every unit is loaded now — run the one closed-
|
# whole-program (jolt-t34): every unit is loaded now — run the one
|
||||||
# world fixpoint over all of them before -main, so cross-ns types propagate
|
# closed-world fixpoint over all of them before -main.
|
||||||
(when (get (ctx :env) :whole-program?)
|
(when (get (ctx :env) :whole-program?)
|
||||||
(when-let [ip (get (ctx :env) :infer-program!)] (protect (ip ctx)))
|
(when-let [ip (get (ctx :env) :infer-program!)] (protect (ip ctx)))
|
||||||
# the batch pass is done — a namespace required later (inside -main) now
|
|
||||||
# falls back to per-ns inference instead of being deferred and never run.
|
|
||||||
(put (ctx :env) :infer-program-done? true))
|
(put (ctx :env) :infer-program-done? true))
|
||||||
|
(save-deps-image ctx path)))
|
||||||
(load-string ctx (string "(apply " ns-name "/-main *command-line-args*)")))
|
(load-string ctx (string "(apply " ns-name "/-main *command-line-args*)")))
|
||||||
([err fib] (report-error err fib) (os/exit 1))))
|
([err fib] (report-error err fib) (os/exit 1))))
|
||||||
|
|
||||||
|
|
@ -533,7 +594,17 @@
|
||||||
:none))
|
:none))
|
||||||
(def dl (case dl-forced :off false :on true (not open-mode?)))
|
(def dl (case dl-forced :off false :on true (not open-mode?)))
|
||||||
(put (ctx :env) :direct-linking? dl)
|
(put (ctx :env) :direct-linking? dl)
|
||||||
(put (ctx :env) :inline? dl)
|
# Inference / specialization (inlining + scalar replacement + structural type
|
||||||
|
# inference + the per-namespace re-emit fixpoint) is the EXPENSIVE part — like
|
||||||
|
# Stalin's whole-program analysis or a Clojure AOT pass, it belongs at BUILD
|
||||||
|
# time or behind an explicit opt-in, not paid on every program startup. Direct-
|
||||||
|
# linking (cheap compile-time call resolution; FASTER startup + calls, per
|
||||||
|
# Clojure's :direct-linking) does not need it. So default it OFF and turn it on
|
||||||
|
# with JOLT_OPTIMIZE (or an explicit JOLT_DIRECT_LINK, which signals a fully
|
||||||
|
# optimized build). The native build bakes an optimized image once.
|
||||||
|
(def optimize? (and dl (truthy? (or (os/getenv "JOLT_OPTIMIZE")
|
||||||
|
(= "1" (os/getenv "JOLT_DIRECT_LINK"))))))
|
||||||
|
(put (ctx :env) :inline? optimize?)
|
||||||
# Mark direct-linking that was AUTO-enabled by the run mode (vs explicitly
|
# Mark direct-linking that was AUTO-enabled by the run mode (vs explicitly
|
||||||
# requested). The success checker (RFC 0006) rides on the inference for free,
|
# requested). The success checker (RFC 0006) rides on the inference for free,
|
||||||
# but a casual program run shouldn't spam type warnings just because it now
|
# but a casual program run shouldn't spam type warnings just because it now
|
||||||
|
|
@ -554,7 +625,7 @@
|
||||||
# other direct-linked modes; JOLT_NO_WHOLE_PROGRAM opts out. Namespaces required
|
# other direct-linked modes; JOLT_NO_WHOLE_PROGRAM opts out. Namespaces required
|
||||||
# later (inside -main) fall back to per-ns inference (see loader).
|
# later (inside -main) fall back to per-ns inference (see loader).
|
||||||
(put (ctx :env) :whole-program?
|
(put (ctx :env) :whole-program?
|
||||||
(and dl
|
(and optimize?
|
||||||
(not (os/getenv "JOLT_NO_WHOLE_PROGRAM"))
|
(not (os/getenv "JOLT_NO_WHOLE_PROGRAM"))
|
||||||
(or main-entry? (os/getenv "JOLT_WHOLE_PROGRAM"))))
|
(or main-entry? (os/getenv "JOLT_WHOLE_PROGRAM"))))
|
||||||
(cond
|
(cond
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue