Drop the compiler image from no-eval binaries (footprint)
An AOT-compiled app only needs the analyzer/back end at runtime to compile from source — eval / load-string / load-file. Macros are expanded at build time and a require of a baked namespace no-ops, so a closed app that never compiles at runtime carries the compiler image (~0.8MB) as dead weight. Under --tree-shake, when reachability is trustworthy (no bail) and no reachable code references eval/load-string/load-file/load-reader/load, omit host/chez/seed/image.ss + compile-eval.ss from the runtime manifest. bld-tree-shake returns the flag alongside the shaken forms; bld-emit-runtime filters the manifest. Measured: build-app 9.84MB -> 9.05MB, still runs. Safety verified: an app that evals keeps the compiler (eval is a bail + compile ref) and eval works at runtime. build-smoke asserts the compiler is gone in the no-eval app; full make test green.
This commit is contained in:
parent
20c5b92ea8
commit
3c5d548b70
3 changed files with 61 additions and 35 deletions
|
|
@ -92,4 +92,8 @@ fi
|
|||
if grep -q 'def-var! "app.util" "twice"' "$out.build/flat.ss"; then
|
||||
echo " FAIL: --tree-shake did not drop the unreachable twice macro"; exit 1
|
||||
fi
|
||||
echo "build smoke: passed (release + optimized + direct-link + tree-shake)"
|
||||
# The app never evals, so the compiler image (analyzer/back end) is dropped.
|
||||
if grep -q 'def-var! "jolt.analyzer"' "$out.build/flat.ss"; then
|
||||
echo " FAIL: --tree-shake kept the compiler image in a no-eval app"; exit 1
|
||||
fi
|
||||
echo "build smoke: passed (release + optimized + direct-link + tree-shake + compiler-drop)"
|
||||
|
|
|
|||
|
|
@ -137,8 +137,15 @@
|
|||
(for-each (lambda (l) (bld-inline-line l out (+ depth 1))) (bld-file-lines p))
|
||||
(begin (put-string out line) (put-string out "\n")))))
|
||||
|
||||
(define (bld-emit-runtime out)
|
||||
(for-each (lambda (l) (bld-inline-line l out 0)) bld-runtime-manifest))
|
||||
;; Inline the runtime manifest. When drop-compiler? (a closed AOT app that never
|
||||
;; compiles from source at runtime), omit the compiler image + compile-eval shim —
|
||||
;; the analyzer/back end are dead weight in the binary (~0.8MB).
|
||||
(define (bld-emit-runtime out drop-compiler?)
|
||||
(for-each (lambda (l)
|
||||
(unless (and drop-compiler?
|
||||
(or (bld-contains? l "seed/image.ss") (bld-contains? l "compile-eval.ss")))
|
||||
(bld-inline-line l out 0)))
|
||||
bld-runtime-manifest))
|
||||
|
||||
;; --- app emission -----------------------------------------------------------
|
||||
;; Re-emit one app namespace to a list of Scheme strings: optimize (run-passes)
|
||||
|
|
@ -168,28 +175,33 @@
|
|||
(begin (hashtable-set! reached fq #t)
|
||||
(bfs (append (or (hashtable-ref edges fq #f) '()) (cdr work))))))))
|
||||
(let ((kept? (lambda (r) (or (vector-ref r 0) (hashtable-ref reached (vector-ref r 1) #f))))
|
||||
(bail #f))
|
||||
(refs-any (lambda (r set) (ormap (lambda (b) (and (member b (vector-ref r 2)) #t)) set)))
|
||||
(bail #f) (needs-compiler #f))
|
||||
;; bail only if REACHABLE code resolves vars by name — then the static call
|
||||
;; graph is incomplete and dropping anything is unsound. Unreached eval-using
|
||||
;; library code is simply shaken away and never triggers the bail.
|
||||
;; library code is simply shaken away and never triggers the bail. Track
|
||||
;; whether any reachable code needs the compiler at runtime (eval/load).
|
||||
(for-each (lambda (r)
|
||||
(when (and (kept? r)
|
||||
(ormap (lambda (b) (and (member b (vector-ref r 2)) #t)) dce-bail-refs))
|
||||
(set! bail #t)))
|
||||
(when (kept? r)
|
||||
(when (refs-any r dce-bail-refs) (set! bail #t))
|
||||
(when (refs-any r dce-compile-refs) (set! needs-compiler #t))))
|
||||
records)
|
||||
(if bail
|
||||
(begin (display "jolt build: tree-shake skipped (reachable code resolves vars at runtime)\n")
|
||||
(map (lambda (r) (vector-ref r 3)) records))
|
||||
(let ((kept '()) (ndef 0) (nkeep 0))
|
||||
(for-each (lambda (r)
|
||||
(when (vector-ref r 1) (set! ndef (+ ndef 1)))
|
||||
(when (kept? r)
|
||||
(when (vector-ref r 1) (set! nkeep (+ nkeep 1)))
|
||||
(set! kept (cons (vector-ref r 3) kept))))
|
||||
records)
|
||||
(display (string-append "jolt build: tree-shake kept " (number->string nkeep)
|
||||
" of " (number->string ndef) " defs\n"))
|
||||
(reverse kept)))))))
|
||||
;; the compiler image can be dropped only when reachability is trustworthy
|
||||
;; (no bail) and no reachable code compiles from source at runtime.
|
||||
(let ((drop-compiler? (and (not bail) (not needs-compiler))))
|
||||
(if bail
|
||||
(begin (display "jolt build: tree-shake skipped (reachable code resolves vars at runtime)\n")
|
||||
(values (map (lambda (r) (vector-ref r 3)) records) drop-compiler?))
|
||||
(let ((kept '()) (ndef 0) (nkeep 0))
|
||||
(for-each (lambda (r)
|
||||
(when (vector-ref r 1) (set! ndef (+ ndef 1)))
|
||||
(when (kept? r)
|
||||
(when (vector-ref r 1) (set! nkeep (+ nkeep 1)))
|
||||
(set! kept (cons (vector-ref r 3) kept))))
|
||||
records)
|
||||
(display (string-append "jolt build: tree-shake kept " (number->string nkeep)
|
||||
" of " (number->string ndef) " defs\n"))
|
||||
(values (reverse kept) drop-compiler?))))))))
|
||||
|
||||
;; --- bundling: native libs + resources --------------------------------------
|
||||
;; A jolt seq of jolt strings -> a Scheme list of Scheme strings.
|
||||
|
|
@ -282,18 +294,20 @@
|
|||
(when direct-link?
|
||||
((var-deref "jolt.backend-scheme" "set-direct-link!") #t)
|
||||
((var-deref "jolt.backend-scheme" "direct-link-reset!")))
|
||||
(let* ((app-strs (if tree-shake?
|
||||
(bld-tree-shake
|
||||
(apply append
|
||||
(map (lambda (nf) (ei-emit-ns-records (car nf) (read-file-string (cdr nf))))
|
||||
ordered))
|
||||
(string-append entry-ns "/-main"))
|
||||
(apply append
|
||||
(map (lambda (nf) (bld-emit-ns (car nf) (read-file-string (cdr nf))))
|
||||
ordered))))
|
||||
(_ (set-optimize! #f))
|
||||
(_ ((var-deref "jolt.backend-scheme" "set-direct-link!") #f))
|
||||
(builddir (string-append out-path ".build"))
|
||||
(let*-values
|
||||
(((app-strs drop-compiler?)
|
||||
(if tree-shake?
|
||||
(bld-tree-shake
|
||||
(apply append
|
||||
(map (lambda (nf) (ei-emit-ns-records (car nf) (read-file-string (cdr nf)))) ordered))
|
||||
(string-append entry-ns "/-main"))
|
||||
(values (apply append
|
||||
(map (lambda (nf) (bld-emit-ns (car nf) (read-file-string (cdr nf)))) ordered))
|
||||
#f))))
|
||||
(set-optimize! #f)
|
||||
((var-deref "jolt.backend-scheme" "set-direct-link!") #f)
|
||||
(when drop-compiler? (display "jolt build: dropping compiler image (no runtime eval)\n"))
|
||||
(let* ((builddir (string-append out-path ".build"))
|
||||
(flat-ss (string-append builddir "/flat.ss"))
|
||||
(flat-so (string-append builddir "/flat.so"))
|
||||
(boot (string-append builddir "/jolt.boot"))
|
||||
|
|
@ -302,7 +316,7 @@
|
|||
(bld-system (string-append "mkdir -p '" builddir "'"))
|
||||
;; 3. flat source = runtime + app + launcher.
|
||||
(let ((out (open-output-file flat-ss 'replace)))
|
||||
(bld-emit-runtime out)
|
||||
(bld-emit-runtime out drop-compiler?)
|
||||
;; Load native libs, bake embedded resources, and point source roots at
|
||||
;; the build-time app roots — all BEFORE the app forms. The app's
|
||||
;; top-level forms run at binary startup (Sbuild_heap), and they include
|
||||
|
|
@ -377,7 +391,7 @@
|
|||
(bld-system (string-append
|
||||
"cc -O2 -I'" bld-csv-dir "' '" main-c "' '" bld-csv-dir "/libkernel.a' "
|
||||
"-o '" out-path "' " (bld-link-libs)))
|
||||
(display (string-append "jolt build: wrote " out-path "\n"))))))
|
||||
(display (string-append "jolt build: wrote " out-path "\n")))))))
|
||||
|
||||
(def-var! "jolt.host" "build-binary"
|
||||
(lambda (entry out mode natives embed-dirs ext-roots direct-link? tree-shake?)
|
||||
|
|
|
|||
|
|
@ -117,6 +117,14 @@
|
|||
"clojure.core/load-string" "clojure.core/load-file" "clojure.core/load-reader"
|
||||
"clojure.core/load"))
|
||||
|
||||
;; A reference that needs the analyzer/back end at runtime (compile-from-source). If
|
||||
;; reachable code uses none of these, the compiler image can be dropped from the
|
||||
;; binary — the AOT app is fully compiled. (resolve/require don't need it: resolve is
|
||||
;; a var-table lookup; a require of a baked ns no-ops.)
|
||||
(define dce-compile-refs
|
||||
'("clojure.core/eval" "clojure.core/load-string" "clojure.core/load-file"
|
||||
"clojure.core/load-reader" "clojure.core/load"))
|
||||
|
||||
;; One record per form: (vector keep? fqn refs str). keep? #t = a non-def form,
|
||||
;; always emitted, its refs are reachability roots; #f = a prunable def emitted only
|
||||
;; if fqn is reached. A macro is a prunable def (its expander isn't called at runtime
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue