Merge pull request #121 from jolt-lang/refactor-phase4b-ctx-image

Refactor phase 4b: share the ctx-image load/save dance (jolt-q5ql)
This commit is contained in:
Dmitri Sotnikov 2026-06-15 13:26:38 +00:00 committed by GitHub
commit 10482dae35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 44 additions and 33 deletions

View file

@ -245,7 +245,7 @@
(and (os/getenv "JOLT_WHOLE_PROGRAM") (get (ctx :env) :direct-linking?))) (and (os/getenv "JOLT_WHOLE_PROGRAM") (get (ctx :env) :direct-linking?)))
ctx)) ctx))
# --- Context snapshot/fork (cheap isolated copies) -------------------------- # --- Context snapshot/fork + disk image (AOT context image) ------------------
# #
# init is expensive (~50 ms interpreted, ~900 ms compiled: tier loading, analyzer # init is expensive (~50 ms interpreted, ~900 ms compiled: tier loading, analyzer
# build, macro recompilation). For workloads that need MANY isolated contexts — # build, macro recompilation). For workloads that need MANY isolated contexts —
@ -254,6 +254,12 @@
# shares nothing mutable with the original: defs, protocol extensions, hierarchy # shares nothing mutable with the original: defs, protocol extensions, hierarchy
# changes, atom states in one fork are invisible to the others. # changes, atom states in one fork are invisible to the others.
# #
# load-ctx-image/save-ctx-image add the disk layer (fork->validate->rewire load,
# atomic save) shared by init-cached and main's deps-image cache (jolt-q5ql) —
# the callers differ only in the validity predicate. They live here (not a
# separate module) because Janet's `use` doesn't transitively re-export, and the
# snapshot/fork they build on are already api's and consumed via (use ./api).
#
# The reverse-lookup dicts must be built from root-env (cfunctions and abstract # The reverse-lookup dicts must be built from root-env (cfunctions and abstract
# values from the Janet runtime marshal by reference through them) BEFORE any ctx # values from the Janet runtime marshal by reference through them) BEFORE any ctx
# values exist in scope — module load time here, so user code can't leak into it. # values exist in scope — module load time here, so user code can't leak into it.
@ -273,8 +279,30 @@
[snap] [snap]
(unmarshal snap image-load-dict)) (unmarshal snap image-load-dict))
# --- Disk-cached init (AOT context image) ------------------------------------ (defn load-ctx-image
# "Load a marshaled ctx image from `path`: fork it, verify it really is a ctx (a
corrupt image can unmarshal to a non-ctx value), run the optional `valid?`
predicate (e.g. a source-mtime check), and replay the per-PROCESS module-state
wiring an image restore skips by calling `(rewire! ctx)` (the print-method hook
lives in module state, not the marshaled ctx). Returns the ctx, or nil if the
file is absent, unreadable, not a ctx, or fails `valid?`."
[path rewire! &opt valid?]
(when (os/stat path)
(def r (protect (fork (slurp path))))
(when (and (r 0) (ctx? (r 1)) (or (nil? valid?) (valid? (r 1))))
(def c (r 1))
(when rewire! (rewire! c))
c)))
(defn save-ctx-image
"Atomically write ctx's marshaled image to `path` (write a pid-tagged tmp file
then rename) so concurrent cold starts never observe a torn image. Best-effort:
a failed write/rename is swallowed (the next run just rebuilds)."
[ctx path]
(def tmp (string path "." (os/getpid) ".tmp"))
(when (protect (spit tmp (snapshot ctx)))
(protect (os/rename tmp path))))
# init in compile mode is ~2.4 s (tier loading, analyzer self-compile, macro # init in compile mode is ~2.4 s (tier loading, analyzer self-compile, macro
# recompilation) — paid by every PROCESS that builds a ctx from source, e.g. # recompilation) — paid by every PROCESS that builds a ctx from source, e.g.
# each `jpm test` file. init-cached pays it once: the built ctx is snapshotted # each `jpm test` file. init-cached pays it once: the built ctx is snapshotted
@ -334,23 +362,12 @@
(default opts {}) (default opts {})
(if (or (= "1" (os/getenv "JOLT_NO_IMAGE_CACHE")) (nil? (src-dir))) (if (or (= "1" (os/getenv "JOLT_NO_IMAGE_CACHE")) (nil? (src-dir)))
(init opts) (init opts)
(let [path (image-cache-path opts) (let [path (image-cache-path opts)]
loaded (when (os/stat path) # The source-fingerprint is already in the path, so any cached image at
(let [r (protect (fork (slurp path)))] # this path is valid — no extra predicate, just the per-process rewiring.
# unmarshal of a corrupt image can also "succeed" with a (or (load-ctx-image path install-print-method-cb!)
# non-ctx value, so check the shape, not just the throw. (let [ctx (init opts)]
(when (and (r 0) (ctx? (r 1))) (save-ctx-image ctx path)
(r 1))))]
(or (when loaded
# per-PROCESS wiring an image restore skips: the renderer's
# print-method hook lives in module state, not the marshaled ctx
(install-print-method-cb! loaded)
loaded)
(let [ctx (init opts)
tmp (string path "." (os/getpid) ".tmp")]
# Atomic publish so concurrent cold starts never see a torn image.
(when (protect (spit tmp (snapshot ctx)))
(protect (os/rename tmp path)))
ctx))))) ctx)))))
(defn eval-one (defn eval-one

View file

@ -461,23 +461,17 @@
ok) ok)
(defn- try-load-deps-image [path] (defn- try-load-deps-image [path]
(when (and (not (os/getenv "JOLT_NO_DEPS_CACHE")) (os/stat path)) (unless (os/getenv "JOLT_NO_DEPS_CACHE")
(def r (protect (fork (slurp path)))) # load-ctx-image forks + validates + rewires (shared with init-cached, jolt-q5ql);
(when (and (r 0) (ctx? (r 1))) # the deps-specific validity check is the source-mtime manifest.
(def c (r 1)) (load-ctx-image path jcore/install-print-method-cb!
(def manifest (get (c :env) :deps-manifest)) (fn [c] (let [m (get (c :env) :deps-manifest)]
(when (and manifest (manifest-current? manifest)) (and m (manifest-current? m)))))))
# 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] (defn- save-deps-image [c path]
(unless (os/getenv "JOLT_NO_DEPS_CACHE") (unless (os/getenv "JOLT_NO_DEPS_CACHE")
(put (c :env) :deps-manifest (manifest-of (or (get (c :env) :loaded-files) @[]))) (put (c :env) :deps-manifest (manifest-of (or (get (c :env) :loaded-files) @[])))
(def tmp (string path "." (os/getpid) ".tmp")) (save-ctx-image c path)))
(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))