Re: Script caching
Per Nyfelt <[email protected]> Wed, 3 Jun 2026 00:11:55 +0200
| Newsgroups | gmane.comp.lang.groovy.devel |
|---|---|
| Organization | Alipsa HB |
| Message-ID | <[email protected]> |
Thanks for the feedback Paul!
Below is an updated proposal (also attached as an md). Next, I am going
to create a wrapper script to validate as many assumption as i can. A
benchmark should hopefully confirm that cache-hit time is meaningfully
lower than fresh groovy invocation time, and that fingerprinting +
class-loading overhead does not negate the saving.
GEP-TBD: Persistent Script Compilation Cache for the Groovy
Command-Line Launcher
Metadata
Field Value
Number GEP-TBD
Title Persistent Script Compilation Cache for the Groovy Command-Line
Launcher
Version 0.2
Type Feature
Status Draft
Leader Per Nyfelt
Created 2026-06-01
Last modification 2026-06-02
Target Groovy version TBD
Discussion TBD
Reference implementation TBD
Abstract
This proposal introduces an optional persistent compilation cache for
Groovyscripts executed through the|groovy|command-line launcher.
When an eligible script is executed,the launcher may store the generated
classfiles in a local cache.On later executions,if the script source and
allrelevant compilation inputs are unchanged,the launcher may load the
cachedclass files instead of parsing,transforming,and compiling the
script again.
The goal is to improve startup time for short-lived Groovy scripts
andcommand-line tools while preserving current semantics.The cache
complementsJVM startup improvements such as CDS and Project Leyden;it is
not intended toreplace them.
Rationale
Groovy is frequently used for scripting,automation,build
tooling,dataprocessing,and command-line utilities.In these use
cases,process lifetime isoften short,and startup overhead matters.
For simple scripts,actual execution time may be negligible compared to:
* JVM startup
* loading the Groovy runtime
* bootstrapping the metaclass registry
* JIT warmup
* parsing the script
* applying AST transformations
* bytecode generation
* class loading
Recent and ongoing JVM work,such as CDS and Project Leyden,can reduce
JVMstartup and class-loading overhead.A persistent script compilation
cache doesnot remove JVM startup,Groovy runtime initialization,or JIT
costs.Its directbenefit is narrower:it can avoid parsing,AST
transformations,and bytecodegeneration for an unchanged script.A cache
hit may also avoid loading parts ofthe compiler frontend.
The realistic performance ceiling is therefore the portion of wall-clock
timespent compiling the script.That portion is likely to be smallest for
trivialscripts and larger for scripts with substantial source or AST
transformationwork.The actual benefit must be measured across
representative scripts.
Groovy already caches compiled script classes during the lifetime of
a|GroovyClassLoader|,but this cache is lost when the process
exits.Apersistent launcher-level cache would allow repeated invocations
of unchangedscripts to skip most compilation work.
This would make Groovy more attractive for command-line scripting and
developertooling,especially when used alongside JVM startup improvements.
Goals
The goals of this proposal are:
1. Reduce repeated startup overhead for unchanged Groovy scripts.
2. Avoid changing Groovy language semantics.
3. Make the feature safe and conservative by default.
4. Provide explicit ways to disable, clear, and inspect the cache.
5. Ensure cache invalidation accounts for relevant compilation inputs.
6. Allow future integration with JVM startup technologies such as CDS
or Leyden-style caches.
7. Preserve a fresh JVM for each invocation, without sharing mutable
runtime state between script executions.
Non-Goals
This proposal does not aim to:
1. Replace|groovyc|.
2. Change Groovy compilation semantics.
3. Cache arbitrary runtime results.
4. Guarantee improved performance for all scripts.
5. Provide a distributed or shared build cache.
6. Cache scripts run inside long-lived embedded Groovy runtimes.
7. Solve dependency resolution caching for|@Grab|, although it must
interact safely with it.
8. Remove JVM startup, Groovy runtime initialization, or JIT warmup costs.
9. Provide a resident JVM or daemon process.
Proposed Behavior
When running:
|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy
myscript.groovy |
when the cache is enabled,the launcher may:
1. Determine whether the script is eligible for caching.
2. If eligible, compute a cache key from the script and compilation
environment.
3. Look for previously generated class files matching that key.
4. If found and valid, load the cached classes.
5. If the script is ineligible or no valid cache entry was found,
compile the script normally and, if eligible, store the generated
classes in the cache.
The user-visible behavior of the script must remain the same as if the
scripthad been compiled from source during that invocation.
Correctness-First Principle
The cache is an optimization.Reusing stale or incompatible bytecode is
arelease-blocking defect.Whenever the launcher cannot establish that
everyrelevant compilation input is unchanged,it must compile the script
normallyinstead of reusing a cache entry.
Initial Scope
The first implementation should be deliberately narrow.It should cache
onlyfile-backed scripts executed through the|groovy|command-line
launcher whenthe compilation environment can be fingerprinted reliably.
The initial implementation should treat the following as ineligible:
* scripts using|@Grab|
* scripts using externally supplied or dynamically mutated compiler
configuration, including configuration scripts, that cannot be
fingerprinted reliably
* scripts affected by launcher configuration, such as|GROOVY_CONF|,
that cannot be fingerprinted reliably
* scripts affected by launcher startup hooks or JVM options when those
hooks or options cannot be fingerprinted reliably
* scripts affected by AST transformations that cannot be fingerprinted
reliably
* scripts with compilation inputs or dependencies that cannot be
identified reliably
An ineligible script must be compiled normally.Ineligibility is a cache
miss,not an error.
Cache Location
The default cache location should follow platform conventions.
Suggested defaults:
Platform Cache location
Linux/Unix
|$XDG_CACHE_HOME/groovy/script-cache|or|~/.cache/groovy/script-cache|
macOS |~/Library/Caches/Groovy/script-cache|
Windows |%LOCALAPPDATA%\Groovy\script-cache|
A system property or environment variable should allow overriding
thelocation:
|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy
-Dgroovy.script.cache.dir=/path/to/cache myscript.groovy
GROOVY_SCRIPT_CACHE_DIR=/path/to/cache groovy myscript.groovy |
Enabling and Disabling
The cache should initially be opt-in unless the Groovy project decides
theinvalidation model is sufficiently conservative for default use.
The proposed command-line options are:
|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy
--script-cache myscript.groovy groovy --no-script-cache myscript.groovy
groovy --clear-script-cache |
|--clear-script-cache|clears all entries in the selected cache directory
andexits.It does not require a script argument.
The proposed system properties are:
|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>-Dgroovy.script.cache=true
-Dgroovy.script.cache=false -Dgroovy.script.cache.dir=/path/to/cache
-Dgroovy.script.cache.maxSizeBytes=<bytes>|
When multiple mechanisms configure the same setting,the precedence
should be:
1. command-line option
2. system property
3. environment variable, such as|GROOVY_SCRIPT_CACHE_DIR|for the cache
location
4. platform default
Cache enablement remains opt-in
through|--script-cache|or|-Dgroovy.script.cache=true|.
If the feature later proves safe and reliable,it could become enabled
bydefault for normal file-based scripts.
Cache Management
The cache must not grow without bound.The implementation should enforce
aconfigurable size limit,such
as|-Dgroovy.script.cache.maxSizeBytes=<bytes>|,and evict entries
automatically when that limit is exceeded.The exact defaultlimit and
eviction policy should be determined by the reference implementation.
Entries from older Groovy versions or cache format versions must be
eligiblefor cleanup.Automatic cleanup should be best-effort and must not
preventnormal script execution.Users can remove all entries in the
selected cachedirectory explicitly with|--clear-script-cache|.
An optional cache-size summary or listing command is discussed underOpen
Questions
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#open-questions>.
Source Identity
Before computing a cache key for a file-backed script,the launcher
shouldresolve the script to its canonical path.Relative paths should
therefore beresolved against the current working directory,and symlink
aliases shouldidentify the same source file.
This matches the existing file-backed|GroovyCodeSource|behavior.
Package Declarations
File-backed scripts with package declarations should remain eligible
forcaching.The source hash already accounts for the package
declaration.Thecache entry must record the binary names of all generated
classes so they canbe defined with the same names and packages on a
cache hit.
Cache Key
The cache key and validation metadata must include enough information to
avoidreusing stale or incompatible bytecode.The fingerprint should
beover-inclusive:uncertainty must cause a cache miss rather than reuse
of anentry that might be stale.
At minimum,the key should include:
* canonical script path
* script source hash
* cache format version
* Groovy version and a fingerprint of the effective Groovy runtime
distribution
* Java version or class file target version
* effective classpath
* compiler configuration
* invokedynamic setting
* |--enable-preview|, the|groovy.preview.features|system property set
by the current Groovy launchers when preview mode is enabled, and
any related compiler or JVM options that affect generated bytecode
* script base class
* active AST transformations
* classpath-discovered extension modules
* effective launcher configuration,
including|GROOVY_HOME|,|groovy.home|,|GROOVY_CONF|, and
configuration scripts where relevant
* relevant system properties that affect compilation
The effective classpath fingerprint must detect changes to
extension-moduledescriptors and their implementation classes.Groovy
discovers extensionmodules from both:
* |META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule|
* |META-INF/services/org.codehaus.groovy.runtime.ExtensionModule|
Launcher startup hooks and JVM options,such as
relevant|JAVA_OPTS|,maychange compilation behavior indirectly.The
fingerprint should representeffective compilation-affecting inputs after
launcher processing rather thanblindly hashing every launcher
environment variable or JVM option.Unrelatedruntime options should not
create unnecessary cache misses.
The Groovy version alone is not sufficient to distinguish custom
orvendor-modified distributions.The distribution fingerprint must
detectchanges to Groovy runtime artifacts that can affect
compilation,even when thereported Groovy version is
unchanged.|GROOVY_HOME|and|groovy.home|should berepresented through
their effective compilation inputs rather than treated aspath strings
when the resulting distribution and configuration are equivalent.
The implementation must also account for dependent Groovy sources
discoveredduring compilation.A changed sibling script must invalidate
any cached scriptthat depends on it.|GroovyScriptEngine|already tracks
source dependenciesduring compilation and should inform this design.
The first implementation should not cache scripts using|@Grab|.A
laterimplementation may reconsider this if resolved
dependencies,compile-timeclasspath mutation,and discovered
transformations can be represented safely.
Fingerprinting must also be efficient enough to preserve the expected
benefit.For example,a full content hash of every classpath entry may be
safe but tooexpensive.The implementation should benchmark competing
strategies and prefera cache miss when a cheap,reliable fingerprint is
unavailable.
Cache Contents
The cache should store:
* generated|.class|files
* binary names of all generated classes
* metadata describing the compilation environment
* cache format version
* source hash
* Groovy version
* Groovy runtime distribution fingerprint
* Java or class file target
* classpath fingerprint
* dependency metadata for discovered Groovy sources
* timestamp of creation
* last-access metadata and entry size where required by the eviction
policy
* optional diagnostic information
The cache format should be treated as internal and may change between
Groovyversions.
Cached classes must retain equivalent|CodeSource|and
protection-domainbehavior when they are loaded in a new process.
Cache Entry Layout
Each cache entry should be a self-contained unit identified by a digest
of itscache key.It should contain the metadata and every generated class
file neededfor that script.Writers should create temporary entries on
the same filesystemas the cache and publish complete entries atomically.
The implementation may shard entries by key prefix to avoid excessively
largedirectories.The exact directory names,metadata encoding,and
sharding schemeare internal cache-format details to be determined by the
referenceimplementation.
Invalidation
A cached script must be invalidated when any relevant compilation
inputchanges.
Examples:
* script source changed
* Groovy version changed
* Groovy runtime distribution changed without a version change
* Java target changed
* classpath changed
* AST transform implementation changed
* compiler configuration changed
* |@Grab|dependencies changed, for a future implementation that
enables|@Grab|caching
* cache format changed
* dependent Groovy source changed
* extension-module descriptor or implementation changed
* compilation-affecting launcher configuration or JVM option changed
If validation fails or metadata is unreadable,the launcher should
silentlyfall back to normal compilation unless diagnostics are enabled.
As required by theCorrectness-First Principle
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#correctness-first-principle>,the
implementation must default to a cache miss whenever it cannot
establishthat all relevant compilation inputs are unchanged.
Diagnostics
The launcher should provide optional diagnostics.
The proposed diagnostic options are:
|<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#>groovy
--script-cache-info myscript.groovy groovy --script-cache-verbose
myscript.groovy |
|--script-cache-info|should report whether the invocation was a
hit,miss,orineligible for caching,together with a concise
reason.|--script-cache-verbose|should include the same result plus
detailed validation diagnostics,such asthe cache entry location and the
compilation inputs that caused a miss or madethe script ineligible.
Example output:
|Groovy script cache: miss Reason: source hash changed |
or:
|Groovy script cache: hit Cache entry: ~/.cache/groovy/script-cache/... |
Diagnostics should be disabled by default to preserve normal script output.
Security Considerations
The cache stores executable bytecode.Therefore:
1. Cache entries should be private to the current user by default.
2. The cache directory should not be world-writable.
3. The launcher should avoid loading cache entries with unsafe permissions.
4. Cache entries should be isolated by operating-system user. If a
configured cache directory may be shared by multiple users, the
launcher must use separate user-specific namespaces or disable caching.
5. The cache should not weaken existing script security assumptions.
6. Cache writes and reads should defend against symlink-based
replacement and similar filesystem races where supported by the
platform.
7. Cached classes should retain equivalent|CodeSource|and
protection-domain behavior when they are loaded in a new process.
The canonical script path and source hash distinguish scripts from
differentprojects.User isolation should come from cache-directory
scoping andpermissions rather than a user-controlled system property.
On systems where permissions cannot be verified reliably,the launcher
maydisable caching or use a more conservative mode.
Concurrency
Multiple processes may execute the same script concurrently.
The implementation should use atomic writes,temporary files,and safe
renamesto avoid corrupted cache entries.
If another process is replacing a cache entry,the launcher may either
waitbriefly,use the previous complete entry if it remains valid,or
compilenormally.Readers should not observe partially written entries.
Failure Handling
The cache is an optimization.Cache failures must not prevent an
otherwisevalid script from running.
If a cache read fails because an entry is
missing,unreadable,invalid,orcorrupt,the launcher should compile the
script normally.If a cache writefails because the disk is
full,permissions are insufficient,or another I/Oerror occurs,the
launcher should run the normally compiled script withoutpersisting the
entry.
Temporary files should be removed on a best-effort basis.Failures should
bereported only when cache diagnostics are enabled unless they prevent
normalscript execution for an unrelated reason.
Interaction with Existing Groovy Facilities
|GroovyShell|
The|groovy|command-line launcher executes scripts
through|GroovyShell|.|GroovyShell|delegates compilation
to|GroovyClassLoader|,so it is part of thenatural execution path for the
cache.The final ownership boundary shouldfollow the reference
implementation rather than be fixed prematurely.
|GroovyClassLoader|
|GroovyClassLoader|already maintains in-memory caches for loaded classes
andcompiled sources.Its source cache uses a key derived from script text
and codesource,allowing repeated compilation requests within one process
to reuse anexisting|Class|instance.
|GroovyClassLoader|can also add synthetic timestamp fields to
generatedclasses when source recompilation is enabled.Those fields
support sourcestaleness checks for classes loaded within an existing
runtime.
The proposed persistent cache complements this machinery by surviving
acrossprocess invocations.It cannot simply serialize the existing source
cache,because that cache stores loaded|Class|instances rather than
portablebytecode artifacts.A persistent cache must capture all generated
class files,store validation metadata,and define the classes safely in a
new process.
Existing hashing,class collection,and timestamp behavior should be
reused orfactored where practical.The implementation should avoid
introducing aparallel staleness model that can disagree with existing
recompilationbehavior.
|GroovyScriptEngine|
|GroovyScriptEngine|is the most similar existing facility.It caches
scriptclasses in memory for long-lived hosts and tracks dependencies
discoveredduring compilation.It walks those dependencies when deciding
whether a scriptmust be recompiled.
The persistent launcher cache should study and reuse or factor
thisdependency-tracking behavior where practical,especially for scripts
thatdepend on sibling Groovy sources.|GroovyScriptEngine|remains an
in-memory,timestamp-based facility and does not itself solve persistence
acrossshort-lived command-line invocations.
|groovyc|
This proposal does not replace|groovyc|.Users who want explicit
ahead-of-timecompilation can continue using|groovyc|.
The script cache is intended for the common case where users execute
sourcescripts directly with the|groovy|command.
|@Grab|
Scripts using|@Grab|may be cached only if the resolved dependency set
can beincluded in the cache key.
The first implementation should disable persistent script caching for
scriptsusing|@Grab|.
AST Transformations
AST transformations affect generated bytecode and must be part of
thecompilation fingerprint.This includes classpath-discovered global
transformsand source-selected local transforms.If this cannot be done
reliably,cachingshould be disabled for affected scripts.
Possible Implementation Approach
One possible implementation is:
1. Add a cache-aware execution path for eligible file-backed scripts
launched by the|groovy|command.
2. Before compilation, determine whether the script is eligible for
caching.
3. If the script is ineligible, compile and run it normally without
caching.
4. For eligible scripts, compute a|ScriptCacheKey|and look for a
matching cache entry.
5. If present, validate the metadata and load every generated class
using an appropriate class loader while preserving
equivalent|CodeSource|and protection-domain behavior.
6. If the cache entry is absent or invalid, compile it as today.
7. For eligible cache misses, reuse or factor existing class collection
and dependency-tracking machinery where practical.
8. Capture all generated bytecode and discovered dependency metadata.
9. Persist generated bytecode and metadata atomically.
Stored metadata is validated before loading cached classes on
futureinvocations.
The implementation should be internal and not expose cache internals as
stablepublic API in the first version.The reference implementation
should determinewhether the cache belongs in the
launcher,|GroovyShell|,|GroovyClassLoader|,or a focused internal
component used by them.
Reference Implementation
A reference implementation has not yet been provided.
Testing
Tests should cover:
* cache miss on first execution
* cache hit on second execution
* invalidation when script source changes
* invalidation when a dependent sibling Groovy source changes
* invalidation when classpath changes
* invalidation when an extension-module descriptor or implementation
changes
* invalidation when Groovy version, runtime distribution, or cache
format changes
* invalidation when a compilation-affecting launcher or JVM option changes
* disabling the cache
* clearing the entire selected cache directory
* configuration precedence
* bounded growth and eviction
* cleanup of entries from older Groovy or cache format versions
* atomic publication of complete cache entries
* concurrent execution
* scripts with imports
* scripts with local classes
* scripts with closures
* scripts using AST transformations
* scripts using different compiler configurations
* scripts using|--enable-preview|
* scripts with package declarations
* equivalent behavior for relative, absolute, and symlinked script paths
* equivalent fingerprinting for|GROOVY_HOME|locations with equivalent
compilation inputs
* ineligible scripts using|@Grab|
* ineligible scripts with compilation inputs that cannot be fingerprinted
* failure fallback to normal compilation
* fallback when cache writes fail because of disk-full, permission, or
I/O errors
* cache directory permission checks where supported
* user isolation when a configured cache location may be shared
* filesystem race and symlink checks where supported
* equivalent|CodeSource|and protection-domain behavior on cache hits
For a representative corpus of eligible scripts,the test suite should
comparea cache hit with a fresh compilation.It should compare every
generated class,not only the main script class.The comparison should
include:
* differential behavior tests
* normalized bytecode comparison that excludes known volatile metadata
* invalidation tests for each supported fingerprint input
Raw byte-for-byte equality is not always an appropriate oracle because
Groovymay embed recompilation timestamps in generated classes.Known
volatilemetadata includes
synthetic|__timeStamp|and|__timeStamp__...|fields addedfor
source-recompilation checks.Any unexplained semantic or
normalized-bytecodedifference must be investigated as a potential
violation of theCorrectness-First Principle
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#correctness-first-principle>.
Performance tests should measure:
* trivial script
* script with imports
* script with AST transforms
* script with a larger source file
* script with a dependency-heavy classpath
* script using|@CompileStatic|
* script using dynamic Groovy features
Benchmarks should report cold execution,uncached execution,and
cache-hitexecution separately.They should also measure fingerprinting
overhead,including the cost of classpath validation,so the cache is not
enabled forcases where validation consumes the expected savings.
Impact
This proposal is intended to be backward compatible.
If disabled,behavior is unchanged.
If enabled,the observable behavior of a script should be equivalent to
normalsource compilation.If the cache cannot guarantee this,it should
not be used.
The initial implementation would add complexity to the command-line
launcherbut should not expose cache internals as stable public API.
Unlike a resident JVM service,the cache preserves a fresh JVM for
everyinvocation.It does persist executable artifacts on disk,so it is
notstateless in the filesystem sense.
Risks
The main risks are:
1. Incorrect cache invalidation.
2. Security issues from loading cached bytecode.
3. Increased launcher complexity.
4. Hard-to-debug behavior if cached bytecode differs from source
compilation.
5. Limited benefit for scripts where runtime dominates startup.
6. Fingerprinting overhead that consumes the expected compilation savings.
These risks can be mitigated by making the feature initially
opt-in,usingtheCorrectness-First Principle
<http://localhost:63342/markdownPreview/1267336452/markdown-preview-index-5rv387jtrf8turjo5dkbbbl5e.html#correctness-first-principle>,providingdiagnostics,and
falling back to normal compilation whenever uncertaintyexists.
Alternatives Considered
Use|groovyc|
Users can already precompile scripts with|groovyc|.However,this changes
theworkflow and removes the convenience of directly
running|.groovy|sourcefiles.
Rely Only on JVM Startup Improvements
JVM-level startup improvements help Groovy,but they do not
removeGroovy-specific parsing,AST transformation,and bytecode generation
costs.
Keep Only In-Memory Caching
Groovy already benefits from in-memory class caching in long-lived
processes.This does not help repeated short-lived invocations of
the|groovy|command.
External Wrapper Tool
An external script runner could implement persistent
caching,butlauncher-level support would be more
discoverable,portable,and consistent.
GroovyServ
GroovyServ uses a resident warmed JVM plus a thin client.It can avoid
JVMstartup,runtime initialization,and repeated compilation costs,so
itsperformance ceiling is substantially higher than that of a persistent
bytecodecache.
The tradeoff is a stateful daemon process:mutable runtime state may be
sharedacross executions,environment and standard streams must be
propagated,andlifecycle and listener security must be managed.A
persistent launcher cacheserves a different use case.Each invocation
runs in a fresh JVM while stillavoiding compilation work when a disk
entry can be validated safely.The twoapproaches are complementary.
Open Questions
The following decisions should be resolved before the proposal advances
from|Draft|to|Accepted|:
* Should the proposed command-line options, diagnostic options, and
system-property names be adopted as written?
* What default cache-size limit and eviction policy should be used?
* Should the first version provide a cache-size summary command, a
cache-entry listing command, or both? A listing command may expose
local script paths and should account for that privacy concern.
* What is the most efficient classpath fingerprinting strategy that
remains conservative enough for cache reuse?
* What is the most efficient Groovy runtime distribution fingerprint
that detects compilation-affecting changes without unnecessary cache
misses?
* Should the cache-aware component live in the
launcher,|GroovyShell|,|GroovyClassLoader|, or a focused internal
component used by them?
Future Work
Future extensions could include:
* enabling the cache by default
* cache statistics
* integration with CDS or Leyden-style JVM caches
* shared cache support for trusted environments
* tooling integrations for workflows that invoke file-backed scripts
* reusable public APIs for embedders
* support for caching generated stubs where applicable
* smarter dependency fingerprinting for|@Grab|
Conclusion
A persistent script compilation cache would address a long-standing pain
pointfor Groovy as a scripting language:repeated startup cost for
short-livedscripts.
By caching generated class files across
invocations,the|groovy|launchercould avoid unnecessary repeated
parsing,AST transformation,and bytecodegeneration when eligible scripts
and their compilation inputs are unchanged.
Implemented conservatively,this feature would preserve Groovy semantics
whilereducing compilation overhead for repeated command-line script
execution.Itsbenefit should be evaluated alongside JVM startup
improvements such as CDS andProject Leyden.
Update History
Version Date Description
0.2 2026-06-02 Refined conservative v1 scope, cache lifecycle,
fingerprinting, security, diagnostics, implementation flow, and testing
requirements.
0.1 2026-06-01 Initial draft formatted as a Groovy Enhancement Proposal.
Best regards,
Per
On 6/2/26 13:04, Paul King wrote:
> Thanks Per for a well thought out proposal. I generally like the idea.
> Below are some Claude thoughts we'd need to factor in when taking this
> idea further. I wouldn't necessarily give the same priority to things
> it mentions (like @Grab) but they are things we need to think through
> carefully none-the-less.
>
> Cheers, Paul.
>
> -----------
>
> Subject: Re: GEP — Persistent Script Compilation Cache for the Groovy launcher
>
> It's a thorough draft and it targets a real pain point. Here's how I'd
> situate it against the facilities we already have (GroovyShell,
> GroovyClassLoader, GroovyScriptEngine) and against GroovyServ, with a
> few things worth addressing before we go further.
>
> **Where the time actually goes**
>
> A `groovy myscript.groovy` invocation pays for: (1) JVM startup, (2)
> loading the Groovy runtime and bootstrapping the MetaClass registry,
> (3) JIT warmup, (4) parsing to AST, (5) AST transforms, (6) bytecode
> generation, (7) loading the script class, and (8) execution.
>
> The proposed cache only ever saves (4)–(6). It doesn't touch (1)–(3),
> which usually dominate for the small, short-lived scripts the
> Motivation leads with. So the realistic ceiling on the benefit is the
> share of wall-clock spent in parse/transform/codegen — smallest for
> trivial scripts, largest for big or `@CompileStatic`/heavy-transform
> scripts (which are also the hardest to invalidate correctly). The
> proposal already defers (1)–(3) to CDS/Leyden, which is the right
> call: this feature is complementary to CDS, not a standalone startup
> story, and I'd pitch it that way.
>
> **Relationship to what we already have**
>
> - *GroovyClassLoader* already has a content-addressed compilation
> cache — it's just in-memory and process-scoped. `genSourceCacheKey`
> hashes the script text, `parseClass(..., shouldCacheSource)` serves
> the cached class, and generated classes carry a synthetic
> `__timeStamp` field that `isSourceNewer` uses for staleness. The
> proposal is essentially "lift that in-memory source cache onto disk."
> The catch: the in-memory key is *only* the source text, because within
> one process the Groovy version, classpath, compiler config, indy flag
> and active transforms are all constant. Persisting across processes
> makes none of those constant — so the entire "Cache Key" section is
> the genuinely new and hard work, and it's exactly the part the current
> cache never had to solve.
>
> - *GroovyShell* is a thin wrapper over GroovyClassLoader (and is what
> the launcher uses), so it adds nothing persistent but is the natural
> insertion point.
>
> - *GroovyScriptEngine* is the closest relative and worth studying: it
> already does dependency-aware invalidation that this GEP
> under-specifies. Each cache entry tracks a dependency set built during
> compilation, and it walks that graph to decide recompilation. That's a
> better model than a single monolithic classpath hash for catching a
> changed sibling script. But GSE is still in-memory and
> timestamp-based, built for long-lived hosts — it does nothing for the
> short-lived CLI case.
>
> So most of the building blocks (content hashing, the timestamp stamp,
> dependency tracking) already exist. I'd strongly prefer extending them
> over growing a parallel mechanism — two timestamp models that can
> disagree is a maintenance trap.
>
> **Versus GroovyServ**
>
> GroovyServ solves a bigger problem a heavier way: a resident warmed
> JVM plus a thin client, which removes (1)–(3) entirely and gets
> (4)–(6) for free from the existing in-memory cache. That's a far
> larger win than a bytecode cache can deliver — by construction this
> cache cannot touch (1)–(3). The trade is everything that comes with a
> stateful daemon: shared mutable state across runs (system properties,
> static initialisers, thread/classloader leaks), environment
> propagation (cwd, env, stdio, exit codes), lifecycle management, and
> the security of a long-lived listener (GroovyServ uses an auth-cookie
> file — a useful precedent for our security section).
>
> The real selling point of this proposal *over* GroovyServ is that it's
> stateless and side-effect-free per run: every invocation is a clean
> process, so it inherits none of the daemon's state-bleed or admin
> overhead — at the price of a much smaller performance ceiling. The two
> aren't mutually exclusive.
>
> **The main risk: key completeness**
>
> This is the make-or-break. Python's `.pyc` and Ruby's bootsnap can
> ship persistent bytecode caches on-by-default because their
> compilation inputs are small and enumerable. Groovy's are open-ended:
> global AST transforms discovered from the classpath (which `@Grab` can
> mutate at compile time), local transforms discovered only during
> compilation, externally supplied
> `CompilerConfiguration`/`GROOVY_CONF`, and a long, non-exhaustive list
> of compilation-affecting system properties. Two consequences:
>
> 1. You often can't compute a correct key without doing the expensive
> work (resolving `@Grab`, running early phases). Conservatively
> disabling the cache for `@Grab` in v1 is the right move.
> 2. The failure mode is silent wrong behaviour — a missed input gives
> stale bytecode that runs differently from a fresh compile, with no
> error. That's strictly worse than a cache miss and the one thing that
> would damage trust in the launcher. The fingerprint must be
> over-inclusive (miss when in doubt), though note a full classpath
> content hash can itself eat the savings, so there's a real balance to
> measure.
>
> A test the GEP doesn't yet state but should, in my view: *for a corpus
> of scripts, a cache hit must produce byte-identical class files to a
> fresh compile.* If that ever fails, a fingerprint input is missing.
>
> **Bottom line**
>
> It fills a real gap none of
> GroovyShell/GroovyClassLoader/GroovyScriptEngine fill — persistence
> across processes — and it's the safe, stateless counterpart to
> GroovyServ's fast, stateful daemon. I'd support pursuing it provided
> we: build on the existing hashing/timestamp/dependency machinery
> rather than a parallel cache; keep it opt-in and
> default-to-miss-on-uncertainty; disable it for `@Grab` initially;
> pitch it alongside CDS rather than as a standalone speed win; and
> treat silent stale bytecode as a release blocker.
>
> On Tue, Jun 2, 2026 at 3:15 AM Per Nyfelt<[email protected]> wrote:
>> Hi,
>>
>> I think it would be nice if we supported caching for Groovy scripts to speed up execution time for subsequent runs. Below is a GEP style proposal for that. What do you think?
>>
>> GEP: Persistent Script Compilation Cache for the Groovy Command-Line Launcher
>>
>> Metadata
>>
>> Type: Feature
>> Status: Draft
>> Target Groovy Version: TBD
>> Author: Per Nyfelt
>> Discussion: TBD
>> Created: 2026-06-01
>>
>> Abstract
>>
>> This proposal introduces an optional persistent compilation cache for Groovy scripts executed through the groovy command-line launcher.
>>
>> When a script is executed, the launcher may store the generated class files in a local cache. On later executions, if the script source and relevant compilation inputs are unchanged, the launcher may load the cached class files instead of parsing, transforming, and compiling the script again.
>>
>> The goal is to improve startup time for short-lived Groovy scripts and command-line tools while preserving current semantics by default.
>>
>> Motivation
>>
>> Groovy is frequently used for scripting, automation, build tooling, data processing, and command-line utilities. In these use cases, process lifetime is often short, and startup overhead matters.
>>
>> For simple scripts, actual execution time may be negligible compared to:
>>
>> JVM startup
>>
>> loading the Groovy runtime
>>
>> parsing the script
>>
>> applying AST transformations
>>
>> bytecode generation
>>
>> class loading
>>
>> Recent and ongoing JVM work, such as CDS and Project Leyden, can reduce JVM startup and class-loading overhead. However, raw Groovy script execution still pays the cost of compiling the script source on every invocation.
>>
>> Groovy already caches compiled script classes during the lifetime of a GroovyClassLoader, but this cache is lost when the process exits. A persistent launcher-level cache would allow repeated invocations of unchanged scripts to skip most compilation work.
>>
>> This would make Groovy more attractive for command-line scripting and developer tooling, especially compared with languages and runtimes that already persist bytecode or compilation artifacts between runs.
>>
>> Goals
>>
>> The goals of this proposal are:
>>
>> Reduce repeated startup overhead for unchanged Groovy scripts.
>>
>> Avoid changing Groovy language semantics.
>>
>> Make the feature safe and conservative by default.
>>
>> Provide explicit ways to disable, clear, and inspect the cache.
>>
>> Ensure cache invalidation accounts for relevant compilation inputs.
>>
>> Allow future integration with JVM startup technologies such as CDS or Leyden-style caches.
>>
>> Non-Goals
>>
>> This proposal does not aim to:
>>
>> Replace groovyc.
>>
>> Change Groovy compilation semantics.
>>
>> Cache arbitrary runtime results.
>>
>> Guarantee improved performance for all scripts.
>>
>> Provide a distributed or shared build cache.
>>
>> Cache scripts run inside long-lived embedded Groovy runtimes unless those runtimes explicitly opt in.
>>
>> Solve dependency resolution caching for @Grab, although it must interact safely with it.
>>
>> Proposed Behavior
>>
>> When running:
>>
>> groovy myscript.groovy
>>
>> the launcher may:
>>
>> Compute a cache key from the script and compilation environment.
>>
>> Look for previously generated class files matching that key.
>>
>> If found and valid, load the cached classes.
>>
>> Otherwise compile the script normally and store the generated classes in the cache.
>>
>> The user-visible behavior of the script must remain the same as if the script had been compiled from source during that invocation.
>>
>> Cache Location
>>
>> The default cache location should follow platform conventions.
>>
>> Suggested defaults:
>>
>> Linux/Unix:
>> $XDG_CACHE_HOME/groovy/script-cache
>> or ~/.cache/groovy/script-cache
>>
>> macOS:
>> ~/Library/Caches/Groovy/script-cache
>>
>> Windows:
>> %LOCALAPPDATA%\Groovy\script-cache
>>
>> A system property or environment variable should allow overriding the location:
>>
>> groovy -Dgroovy.script.cache.dir=/path/to/cache myscript.groovy
>>
>> Possible environment variable:
>>
>> GROOVY_SCRIPT_CACHE_DIR=/path/to/cache
>>
>> Enabling and Disabling
>>
>> The cache should initially be opt-in unless the Groovy project decides the invalidation model is sufficiently conservative for default use.
>>
>> Possible command-line options:
>>
>> groovy --script-cache myscript.groovy
>> groovy --no-script-cache myscript.groovy
>> groovy --clear-script-cache
>>
>> Possible system properties:
>>
>> -Dgroovy.script.cache=true
>> -Dgroovy.script.cache=false
>> -Dgroovy.script.cache.dir=/path/to/cache
>>
>> If the feature later proves safe and reliable, it could become enabled by default for normal file-based scripts.
>>
>> Cache Key
>>
>> The cache key must include enough information to avoid reusing stale or incompatible bytecode.
>>
>> At minimum, the key should include:
>>
>> absolute or canonical script path
>>
>> script source hash
>>
>> Groovy version
>>
>> Java version or class file target version
>>
>> effective classpath
>>
>> compiler configuration
>>
>> invokedynamic setting
>>
>> preview/incubating compiler flags where relevant
>>
>> script base class
>>
>> active AST transformations
>>
>> relevant system properties that affect compilation
>>
>> For scripts using @Grab, the resolved dependency coordinates and artifact versions should be included after dependency resolution.
>>
>> A conservative implementation may choose to skip caching when the compilation environment cannot be reliably fingerprinted.
>>
>> Cache Contents
>>
>> The cache should store:
>>
>> generated .class files
>>
>> metadata describing the compilation environment
>>
>> cache format version
>>
>> source hash
>>
>> Groovy version
>>
>> Java/classfile target
>>
>> classpath fingerprint
>>
>> timestamp of creation
>>
>> optional diagnostic information
>>
>> The cache format should be treated as internal and may change between Groovy versions.
>>
>> Invalidation
>>
>> A cached script must be invalidated when any relevant compilation input changes.
>>
>> Examples:
>>
>> script source changed
>>
>> Groovy version changed
>>
>> Java target changed
>>
>> classpath changed
>>
>> AST transform implementation changed
>>
>> compiler configuration changed
>>
>> @Grab dependencies changed
>>
>> cache format changed
>>
>> If validation fails or metadata is unreadable, the launcher should silently fall back to normal compilation unless diagnostics are enabled.
>>
>> Diagnostics
>>
>> The launcher should provide optional diagnostics.
>>
>> Examples:
>>
>> groovy --script-cache-info myscript.groovy
>> groovy --script-cache-verbose myscript.groovy
>>
>> Possible output:
>>
>> Groovy script cache: miss
>> Reason: source hash changed
>>
>> or:
>>
>> Groovy script cache: hit
>> Cache entry: ~/.cache/groovy/script-cache/...
>>
>> Diagnostics should be disabled by default to preserve normal script output.
>>
>> Security Considerations
>>
>> The cache stores executable bytecode. Therefore:
>>
>> Cache entries should be private to the current user by default.
>>
>> The cache directory should not be world-writable.
>>
>> The launcher should avoid loading cache entries with unsafe permissions.
>>
>> Cache keys should prevent cross-user or cross-project collisions.
>>
>> The cache should not weaken existing script security assumptions.
>>
>> On systems where permissions cannot be verified reliably, the launcher may disable caching or use a more conservative mode.
>>
>> Concurrency
>>
>> Multiple processes may execute the same script concurrently.
>>
>> The implementation should use atomic writes, temporary files, and safe renames to avoid corrupted cache entries.
>>
>> If a cache entry is being written by another process, the launcher may either wait briefly, ignore the incomplete entry, or compile normally.
>>
>> Interaction with Existing Groovy Facilities
>>
>> GroovyClassLoader
>>
>> The existing in-memory class cache remains useful within a single JVM process. The proposed persistent cache complements it by surviving across process invocations.
>>
>> groovyc
>>
>> This proposal does not replace groovyc. Users who want explicit ahead-of-time compilation can continue using groovyc.
>>
>> The script cache is intended for the common case where users execute source scripts directly with the groovy command.
>>
>> @Grab
>>
>> Scripts using @Grab may be cached only if the resolved dependency set can be included in the cache key.
>>
>> A first implementation may conservatively disable persistent script caching for scripts using @Grab.
>>
>> AST Transformations
>>
>> AST transformations affect generated bytecode and must be part of the compilation fingerprint. If this cannot be done reliably, caching should be disabled for affected scripts.
>>
>> Possible Implementation Approach
>>
>> One possible implementation is:
>>
>> Extend the groovy launcher with a cache-aware script runner.
>>
>> Before compilation, compute a ScriptCacheKey.
>>
>> Look for a matching cache entry.
>>
>> If present, load generated classes using an appropriate class loader.
>>
>> If absent, compile the script as today.
>>
>> Capture generated bytecode.
>>
>> Persist generated bytecode and metadata atomically.
>>
>> On future runs, validate metadata before loading.
>>
>> The implementation should be internal and not expose cache internals as stable public API in the first version.
>>
>> Testing
>>
>> Tests should cover:
>>
>> cache miss on first execution
>>
>> cache hit on second execution
>>
>> invalidation when script source changes
>>
>> invalidation when classpath changes
>>
>> invalidation when Groovy version or cache format changes
>>
>> disabling the cache
>>
>> clearing the cache
>>
>> concurrent execution
>>
>> scripts with imports
>>
>> scripts with local classes
>>
>> scripts using AST transformations
>>
>> scripts using different compiler configurations
>>
>> failure fallback to normal compilation
>>
>> cache directory permission checks where supported
>>
>> Performance tests should measure:
>>
>> trivial script
>>
>> script with imports
>>
>> script with AST transforms
>>
>> script with larger source file
>>
>> script with dependency-heavy classpath
>>
>> script using @CompileStatic
>>
>> script using dynamic Groovy features
>>
>> Backward Compatibility
>>
>> This proposal should be backward compatible.
>>
>> If disabled, behavior is unchanged.
>>
>> If enabled, the observable behavior of a script should be equivalent to normal source compilation. If the cache cannot guarantee this, it should not be used.
>>
>> Risks
>>
>> The main risks are:
>>
>> Incorrect cache invalidation.
>>
>> Security issues from loading cached bytecode.
>>
>> Increased launcher complexity.
>>
>> Hard-to-debug behavior if cached bytecode differs from source compilation.
>>
>> Limited benefit for scripts where runtime dominates startup.
>>
>> These risks can be mitigated by making the feature initially opt-in, using conservative invalidation, providing diagnostics, and falling back to normal compilation whenever uncertainty exists.
>>
>> Alternatives Considered
>>
>> Use groovyc
>>
>> Users can already precompile scripts with groovyc. However, this changes the workflow and removes the convenience of directly running .groovy source files.
>>
>> Rely Only on JVM Startup Improvements
>>
>> JVM-level startup improvements help Groovy, but they do not remove Groovy-specific parsing, AST transformation, and bytecode generation costs.
>>
>> Keep Only In-Memory Caching
>>
>> Groovy already benefits from in-memory class caching in long-lived processes. This does not help repeated short-lived invocations of the groovy command.
>>
>> External Wrapper Tool
>>
>> An external script runner could implement persistent caching, but launcher-level support would be more discoverable, portable, and consistent.
>>
>> Future Work
>>
>> Future extensions could include:
>>
>> enabling the cache by default
>>
>> cache statistics
>>
>> integration with CDS or Leyden-style JVM caches
>>
>> shared cache support for trusted environments
>>
>> Gradle/Maven integration
>>
>> reusable public APIs for embedders
>>
>> support for caching generated stubs where applicable
>>
>> smarter dependency fingerprinting for @Grab
>>
>> Conclusion
>>
>> A persistent script compilation cache would address a long-standing pain point for Groovy as a scripting language: repeated startup cost for short-lived scripts.
>>
>> By caching generated class files across invocations, the groovy launcher could avoid unnecessary repeated parsing, AST transformation, and bytecode generation when scripts are unchanged.
>>
>> Implemented conservatively, this feature would preserve Groovy semantics while making Groovy scripts feel significantly faster in day-to-day command-line use.
>>
>>
scripting-cache-gep.md
(text/markdown, 29.7 KB)
# GEP-TBD: Persistent Script Compilation Cache for the Groovy Command-Line Launcher ## Metadata | Field | Value | |--------------------------|--------------------------------------------------------------------------| | Number | GEP-TBD | | Title | Persistent Script Compilation Cache for the Groovy Command-Line Launcher | | Version | 0.2 | | Type | Feature | | Status | Draft | | Leader | Per Nyfelt | | Created | 2026-06-01 | | Last modification | 2026-06-02 | | Target Groovy version | TBD | | Discussion | TBD | | Reference implementation | TBD | ## Abstract This proposal introduces an optional persistent compilation cache for Groovy scripts executed through the `groovy` command-line launcher. When an eligible script is executed, the launcher may store the generated class files in a local cache. On later executions, if the script source and all relevant compilation inputs are unchanged, the launcher may load the cached class files instead of parsing, transforming, and compiling the script again. The goal is to improve startup time for short-lived Groovy scripts and command-line tools while preserving current semantics. The cache complements JVM startup improvements such as CDS and Project Leyden; it is not intended to replace them. ## Rationale Groovy is frequently used for scripting, automation, build tooling, data processing, and command-line utilities. In these use cases, process lifetime is often short, and startup overhead matters. For simple scripts, actual execution time may be negligible compared to: * JVM startup * loading the Groovy runtime * bootstrapping the metaclass registry * JIT warmup * parsing the script * applying AST transformations * bytecode generation * class loading Recent and ongoing JVM work, such as CDS and Project Leyden, can reduce JVM startup and class-loading overhead. A persistent script compilation cache does not remove JVM startup, Groovy runtime initialization, or JIT costs. Its direct benefit is narrower: it can avoid parsing, AST transformations, and bytecode generation for an unchanged script. A cache hit may also avoid loading parts of the compiler frontend. The realistic performance ceiling is therefore the portion of wall-clock time spent compiling the script. That portion is likely to be smallest for trivial scripts and larger for scripts with substantial source or AST transformation work. The actual benefit must be measured across representative scripts. Groovy already caches compiled script classes during the lifetime of a `GroovyClassLoader`, but this cache is lost when the process exits. A persistent launcher-level cache would allow repeated invocations of unchanged scripts to skip most compilation work. This would make Groovy more attractive for command-line scripting and developer tooling, especially when used alongside JVM startup improvements. ## Goals The goals of this proposal are: 1. Reduce repeated startup overhead for unchanged Groovy scripts. 2. Avoid changing Groovy language semantics. 3. Make the feature safe and conservative by default. 4. Provide explicit ways to disable, clear, and inspect the cache. 5. Ensure cache invalidation accounts for relevant compilation inputs. 6. Allow future integration with JVM startup technologies such as CDS or Leyden-style caches. 7. Preserve a fresh JVM for each invocation, without sharing mutable runtime state between script executions. ## Non-Goals This proposal does not aim to: 1. Replace `groovyc`. 2. Change Groovy compilation semantics. 3. Cache arbitrary runtime results. 4. Guarantee improved performance for all scripts. 5. Provide a distributed or shared build cache. 6. Cache scripts run inside long-lived embedded Groovy runtimes. 7. Solve dependency resolution caching for `@Grab`, although it must interact safely with it. 8. Remove JVM startup, Groovy runtime initialization, or JIT warmup costs. 9. Provide a resident JVM or daemon process. ## Proposed Behavior When running: ```shell groovy myscript.groovy ``` when the cache is enabled, the launcher may: 1. Determine whether the script is eligible for caching. 2. If eligible, compute a cache key from the script and compilation environment. 3. Look for previously generated class files matching that key. 4. If found and valid, load the cached classes. 5. If the script is ineligible or no valid cache entry was found, compile the script normally and, if eligible, store the generated classes in the cache. The user-visible behavior of the script must remain the same as if the script had been compiled from source during that invocation. ### Correctness-First Principle The cache is an optimization. Reusing stale or incompatible bytecode is a release-blocking defect. Whenever the launcher cannot establish that every relevant compilation input is unchanged, it must compile the script normally instead of reusing a cache entry. ### Initial Scope The first implementation should be deliberately narrow. It should cache only file-backed scripts executed through the `groovy` command-line launcher when the compilation environment can be fingerprinted reliably. The initial implementation should treat the following as ineligible: * scripts using `@Grab` * scripts using externally supplied or dynamically mutated compiler configuration, including configuration scripts, that cannot be fingerprinted reliably * scripts affected by launcher configuration, such as `GROOVY_CONF`, that cannot be fingerprinted reliably * scripts affected by launcher startup hooks or JVM options when those hooks or options cannot be fingerprinted reliably * scripts affected by AST transformations that cannot be fingerprinted reliably * scripts with compilation inputs or dependencies that cannot be identified reliably An ineligible script must be compiled normally. Ineligibility is a cache miss, not an error. ### Cache Location The default cache location should follow platform conventions. Suggested defaults: | Platform | Cache location | | --- | --- | | Linux/Unix | `$XDG_CACHE_HOME/groovy/script-cache` or `~/.cache/groovy/script-cache` | | macOS | `~/Library/Caches/Groovy/script-cache` | | Windows | `%LOCALAPPDATA%\Groovy\script-cache` | A system property or environment variable should allow overriding the location: ```shell groovy -Dgroovy.script.cache.dir=/path/to/cache myscript.groovy GROOVY_SCRIPT_CACHE_DIR=/path/to/cache groovy myscript.groovy ``` ### Enabling and Disabling The cache should initially be opt-in unless the Groovy project decides the invalidation model is sufficiently conservative for default use. The proposed command-line options are: ```shell groovy --script-cache myscript.groovy groovy --no-script-cache myscript.groovy groovy --clear-script-cache ``` `--clear-script-cache` clears all entries in the selected cache directory and exits. It does not require a script argument. The proposed system properties are: ```shell -Dgroovy.script.cache=true -Dgroovy.script.cache=false -Dgroovy.script.cache.dir=/path/to/cache -Dgroovy.script.cache.maxSizeBytes=<bytes> ``` When multiple mechanisms configure the same setting, the precedence should be: 1. command-line option 2. system property 3. environment variable, such as `GROOVY_SCRIPT_CACHE_DIR` for the cache location 4. platform default Cache enablement remains opt-in through `--script-cache` or `-Dgroovy.script.cache=true`. If the feature later proves safe and reliable, it could become enabled by default for normal file-based scripts. ### Cache Management The cache must not grow without bound. The implementation should enforce a configurable size limit, such as `-Dgroovy.script.cache.maxSizeBytes=<bytes>`, and evict entries automatically when that limit is exceeded. The exact default limit and eviction policy should be determined by the reference implementation. Entries from older Groovy versions or cache format versions must be eligible for cleanup. Automatic cleanup should be best-effort and must not prevent normal script execution. Users can remove all entries in the selected cache directory explicitly with `--clear-script-cache`. An optional cache-size summary or listing command is discussed under [Open Questions](#open-questions). ### Source Identity Before computing a cache key for a file-backed script, the launcher should resolve the script to its canonical path. Relative paths should therefore be resolved against the current working directory, and symlink aliases should identify the same source file. This matches the existing file-backed `GroovyCodeSource` behavior. ### Package Declarations File-backed scripts with package declarations should remain eligible for caching. The source hash already accounts for the package declaration. The cache entry must record the binary names of all generated classes so they can be defined with the same names and packages on a cache hit. ### Cache Key The cache key and validation metadata must include enough information to avoid reusing stale or incompatible bytecode. The fingerprint should be over-inclusive: uncertainty must cause a cache miss rather than reuse of an entry that might be stale. At minimum, the key should include: * canonical script path * script source hash * cache format version * Groovy version and a fingerprint of the effective Groovy runtime distribution * Java version or class file target version * effective classpath * compiler configuration * invokedynamic setting * `--enable-preview`, the `groovy.preview.features` system property set by the current Groovy launchers when preview mode is enabled, and any related compiler or JVM options that affect generated bytecode * script base class * active AST transformations * classpath-discovered extension modules * effective launcher configuration, including `GROOVY_HOME`, `groovy.home`, `GROOVY_CONF`, and configuration scripts where relevant * relevant system properties that affect compilation The effective classpath fingerprint must detect changes to extension-module descriptors and their implementation classes. Groovy discovers extension modules from both: * `META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule` * `META-INF/services/org.codehaus.groovy.runtime.ExtensionModule` Launcher startup hooks and JVM options, such as relevant `JAVA_OPTS`, may change compilation behavior indirectly. The fingerprint should represent effective compilation-affecting inputs after launcher processing rather than blindly hashing every launcher environment variable or JVM option. Unrelated runtime options should not create unnecessary cache misses. The Groovy version alone is not sufficient to distinguish custom or vendor-modified distributions. The distribution fingerprint must detect changes to Groovy runtime artifacts that can affect compilation, even when the reported Groovy version is unchanged. `GROOVY_HOME` and `groovy.home` should be represented through their effective compilation inputs rather than treated as path strings when the resulting distribution and configuration are equivalent. The implementation must also account for dependent Groovy sources discovered during compilation. A changed sibling script must invalidate any cached script that depends on it. `GroovyScriptEngine` already tracks source dependencies during compilation and should inform this design. The first implementation should not cache scripts using `@Grab`. A later implementation may reconsider this if resolved dependencies, compile-time classpath mutation, and discovered transformations can be represented safely. Fingerprinting must also be efficient enough to preserve the expected benefit. For example, a full content hash of every classpath entry may be safe but too expensive. The implementation should benchmark competing strategies and prefer a cache miss when a cheap, reliable fingerprint is unavailable. ### Cache Contents The cache should store: * generated `.class` files * binary names of all generated classes * metadata describing the compilation environment * cache format version * source hash * Groovy version * Groovy runtime distribution fingerprint * Java or class file target * classpath fingerprint * dependency metadata for discovered Groovy sources * timestamp of creation * last-access metadata and entry size where required by the eviction policy * optional diagnostic information The cache format should be treated as internal and may change between Groovy versions. Cached classes must retain equivalent `CodeSource` and protection-domain behavior when they are loaded in a new process. ### Cache Entry Layout Each cache entry should be a self-contained unit identified by a digest of its cache key. It should contain the metadata and every generated class file needed for that script. Writers should create temporary entries on the same filesystem as the cache and publish complete entries atomically. The implementation may shard entries by key prefix to avoid excessively large directories. The exact directory names, metadata encoding, and sharding scheme are internal cache-format details to be determined by the reference implementation. ### Invalidation A cached script must be invalidated when any relevant compilation input changes. Examples: * script source changed * Groovy version changed * Groovy runtime distribution changed without a version change * Java target changed * classpath changed * AST transform implementation changed * compiler configuration changed * `@Grab` dependencies changed, for a future implementation that enables `@Grab` caching * cache format changed * dependent Groovy source changed * extension-module descriptor or implementation changed * compilation-affecting launcher configuration or JVM option changed If validation fails or metadata is unreadable, the launcher should silently fall back to normal compilation unless diagnostics are enabled. As required by the [Correctness-First Principle](#correctness-first-principle), the implementation must default to a cache miss whenever it cannot establish that all relevant compilation inputs are unchanged. ### Diagnostics The launcher should provide optional diagnostics. The proposed diagnostic options are: ```shell groovy --script-cache-info myscript.groovy groovy --script-cache-verbose myscript.groovy ``` `--script-cache-info` should report whether the invocation was a hit, miss, or ineligible for caching, together with a concise reason. `--script-cache-verbose` should include the same result plus detailed validation diagnostics, such as the cache entry location and the compilation inputs that caused a miss or made the script ineligible. Example output: ```text Groovy script cache: miss Reason: source hash changed ``` or: ```text Groovy script cache: hit Cache entry: ~/.cache/groovy/script-cache/... ``` Diagnostics should be disabled by default to preserve normal script output. ## Security Considerations The cache stores executable bytecode. Therefore: 1. Cache entries should be private to the current user by default. 2. The cache directory should not be world-writable. 3. The launcher should avoid loading cache entries with unsafe permissions. 4. Cache entries should be isolated by operating-system user. If a configured cache directory may be shared by multiple users, the launcher must use separate user-specific namespaces or disable caching. 5. The cache should not weaken existing script security assumptions. 6. Cache writes and reads should defend against symlink-based replacement and similar filesystem races where supported by the platform. 7. Cached classes should retain equivalent `CodeSource` and protection-domain behavior when they are loaded in a new process. The canonical script path and source hash distinguish scripts from different projects. User isolation should come from cache-directory scoping and permissions rather than a user-controlled system property. On systems where permissions cannot be verified reliably, the launcher may disable caching or use a more conservative mode. ## Concurrency Multiple processes may execute the same script concurrently. The implementation should use atomic writes, temporary files, and safe renames to avoid corrupted cache entries. If another process is replacing a cache entry, the launcher may either wait briefly, use the previous complete entry if it remains valid, or compile normally. Readers should not observe partially written entries. ## Failure Handling The cache is an optimization. Cache failures must not prevent an otherwise valid script from running. If a cache read fails because an entry is missing, unreadable, invalid, or corrupt, the launcher should compile the script normally. If a cache write fails because the disk is full, permissions are insufficient, or another I/O error occurs, the launcher should run the normally compiled script without persisting the entry. Temporary files should be removed on a best-effort basis. Failures should be reported only when cache diagnostics are enabled unless they prevent normal script execution for an unrelated reason. ## Interaction with Existing Groovy Facilities ### `GroovyShell` The `groovy` command-line launcher executes scripts through `GroovyShell`. `GroovyShell` delegates compilation to `GroovyClassLoader`, so it is part of the natural execution path for the cache. The final ownership boundary should follow the reference implementation rather than be fixed prematurely. ### `GroovyClassLoader` `GroovyClassLoader` already maintains in-memory caches for loaded classes and compiled sources. Its source cache uses a key derived from script text and code source, allowing repeated compilation requests within one process to reuse an existing `Class` instance. `GroovyClassLoader` can also add synthetic timestamp fields to generated classes when source recompilation is enabled. Those fields support source staleness checks for classes loaded within an existing runtime. The proposed persistent cache complements this machinery by surviving across process invocations. It cannot simply serialize the existing source cache, because that cache stores loaded `Class` instances rather than portable bytecode artifacts. A persistent cache must capture all generated class files, store validation metadata, and define the classes safely in a new process. Existing hashing, class collection, and timestamp behavior should be reused or factored where practical. The implementation should avoid introducing a parallel staleness model that can disagree with existing recompilation behavior. ### `GroovyScriptEngine` `GroovyScriptEngine` is the most similar existing facility. It caches script classes in memory for long-lived hosts and tracks dependencies discovered during compilation. It walks those dependencies when deciding whether a script must be recompiled. The persistent launcher cache should study and reuse or factor this dependency-tracking behavior where practical, especially for scripts that depend on sibling Groovy sources. `GroovyScriptEngine` remains an in-memory, timestamp-based facility and does not itself solve persistence across short-lived command-line invocations. ### `groovyc` This proposal does not replace `groovyc`. Users who want explicit ahead-of-time compilation can continue using `groovyc`. The script cache is intended for the common case where users execute source scripts directly with the `groovy` command. ### `@Grab` Scripts using `@Grab` may be cached only if the resolved dependency set can be included in the cache key. The first implementation should disable persistent script caching for scripts using `@Grab`. ### AST Transformations AST transformations affect generated bytecode and must be part of the compilation fingerprint. This includes classpath-discovered global transforms and source-selected local transforms. If this cannot be done reliably, caching should be disabled for affected scripts. ## Possible Implementation Approach One possible implementation is: 1. Add a cache-aware execution path for eligible file-backed scripts launched by the `groovy` command. 2. Before compilation, determine whether the script is eligible for caching. 3. If the script is ineligible, compile and run it normally without caching. 4. For eligible scripts, compute a `ScriptCacheKey` and look for a matching cache entry. 5. If present, validate the metadata and load every generated class using an appropriate class loader while preserving equivalent `CodeSource` and protection-domain behavior. 6. If the cache entry is absent or invalid, compile it as today. 7. For eligible cache misses, reuse or factor existing class collection and dependency-tracking machinery where practical. 8. Capture all generated bytecode and discovered dependency metadata. 9. Persist generated bytecode and metadata atomically. Stored metadata is validated before loading cached classes on future invocations. The implementation should be internal and not expose cache internals as stable public API in the first version. The reference implementation should determine whether the cache belongs in the launcher, `GroovyShell`, `GroovyClassLoader`, or a focused internal component used by them. ## Reference Implementation A reference implementation has not yet been provided. ## Testing Tests should cover: * cache miss on first execution * cache hit on second execution * invalidation when script source changes * invalidation when a dependent sibling Groovy source changes * invalidation when classpath changes * invalidation when an extension-module descriptor or implementation changes * invalidation when Groovy version, runtime distribution, or cache format changes * invalidation when a compilation-affecting launcher or JVM option changes * disabling the cache * clearing the entire selected cache directory * configuration precedence * bounded growth and eviction * cleanup of entries from older Groovy or cache format versions * atomic publication of complete cache entries * concurrent execution * scripts with imports * scripts with local classes * scripts with closures * scripts using AST transformations * scripts using different compiler configurations * scripts using `--enable-preview` * scripts with package declarations * equivalent behavior for relative, absolute, and symlinked script paths * equivalent fingerprinting for `GROOVY_HOME` locations with equivalent compilation inputs * ineligible scripts using `@Grab` * ineligible scripts with compilation inputs that cannot be fingerprinted * failure fallback to normal compilation * fallback when cache writes fail because of disk-full, permission, or I/O errors * cache directory permission checks where supported * user isolation when a configured cache location may be shared * filesystem race and symlink checks where supported * equivalent `CodeSource` and protection-domain behavior on cache hits For a representative corpus of eligible scripts, the test suite should compare a cache hit with a fresh compilation. It should compare every generated class, not only the main script class. The comparison should include: * differential behavior tests * normalized bytecode comparison that excludes known volatile metadata * invalidation tests for each supported fingerprint input Raw byte-for-byte equality is not always an appropriate oracle because Groovy may embed recompilation timestamps in generated classes. Known volatile metadata includes synthetic `__timeStamp` and `__timeStamp__...` fields added for source-recompilation checks. Any unexplained semantic or normalized-bytecode difference must be investigated as a potential violation of the [Correctness-First Principle](#correctness-first-principle). Performance tests should measure: * trivial script * script with imports * script with AST transforms * script with a larger source file * script with a dependency-heavy classpath * script using `@CompileStatic` * script using dynamic Groovy features Benchmarks should report cold execution, uncached execution, and cache-hit execution separately. They should also measure fingerprinting overhead, including the cost of classpath validation, so the cache is not enabled for cases where validation consumes the expected savings. ## Impact This proposal is intended to be backward compatible. If disabled, behavior is unchanged. If enabled, the observable behavior of a script should be equivalent to normal source compilation. If the cache cannot guarantee this, it should not be used. The initial implementation would add complexity to the command-line launcher but should not expose cache internals as stable public API. Unlike a resident JVM service, the cache preserves a fresh JVM for every invocation. It does persist executable artifacts on disk, so it is not stateless in the filesystem sense. ## Risks The main risks are: 1. Incorrect cache invalidation. 2. Security issues from loading cached bytecode. 3. Increased launcher complexity. 4. Hard-to-debug behavior if cached bytecode differs from source compilation. 5. Limited benefit for scripts where runtime dominates startup. 6. Fingerprinting overhead that consumes the expected compilation savings. These risks can be mitigated by making the feature initially opt-in, using the [Correctness-First Principle](#correctness-first-principle), providing diagnostics, and falling back to normal compilation whenever uncertainty exists. ## Alternatives Considered ### Use `groovyc` Users can already precompile scripts with `groovyc`. However, this changes the workflow and removes the convenience of directly running `.groovy` source files. ### Rely Only on JVM Startup Improvements JVM-level startup improvements help Groovy, but they do not remove Groovy-specific parsing, AST transformation, and bytecode generation costs. ### Keep Only In-Memory Caching Groovy already benefits from in-memory class caching in long-lived processes. This does not help repeated short-lived invocations of the `groovy` command. ### External Wrapper Tool An external script runner could implement persistent caching, but launcher-level support would be more discoverable, portable, and consistent. ### GroovyServ GroovyServ uses a resident warmed JVM plus a thin client. It can avoid JVM startup, runtime initialization, and repeated compilation costs, so its performance ceiling is substantially higher than that of a persistent bytecode cache. The tradeoff is a stateful daemon process: mutable runtime state may be shared across executions, environment and standard streams must be propagated, and lifecycle and listener security must be managed. A persistent launcher cache serves a different use case. Each invocation runs in a fresh JVM while still avoiding compilation work when a disk entry can be validated safely. The two approaches are complementary. ## Open Questions The following decisions should be resolved before the proposal advances from `Draft` to `Accepted`: * Should the proposed command-line options, diagnostic options, and system-property names be adopted as written? * What default cache-size limit and eviction policy should be used? * Should the first version provide a cache-size summary command, a cache-entry listing command, or both? A listing command may expose local script paths and should account for that privacy concern. * What is the most efficient classpath fingerprinting strategy that remains conservative enough for cache reuse? * What is the most efficient Groovy runtime distribution fingerprint that detects compilation-affecting changes without unnecessary cache misses? * Should the cache-aware component live in the launcher, `GroovyShell`, `GroovyClassLoader`, or a focused internal component used by them? ## Future Work Future extensions could include: * enabling the cache by default * cache statistics * integration with CDS or Leyden-style JVM caches * shared cache support for trusted environments * tooling integrations for workflows that invoke file-backed scripts * reusable public APIs for embedders * support for caching generated stubs where applicable * smarter dependency fingerprinting for `@Grab` ## Conclusion A persistent script compilation cache would address a long-standing pain point for Groovy as a scripting language: repeated startup cost for short-lived scripts. By caching generated class files across invocations, the `groovy` launcher could avoid unnecessary repeated parsing, AST transformation, and bytecode generation when eligible scripts and their compilation inputs are unchanged. Implemented conservatively, this feature would preserve Groovy semantics while reducing compilation overhead for repeated command-line script execution. Its benefit should be evaluated alongside JVM startup improvements such as CDS and Project Leyden. ## Update History | Version | Date | Description | |---------|------------|---------------------------------------------------------------------------------------------------------------------------------------| | 0.2 | 2026-06-02 | Refined conservative v1 scope, cache lifecycle, fingerprinting, security, diagnostics, implementation flow, and testing requirements. | | 0.1 | 2026-06-01 | Initial draft formatted as a Groovy Enhancement Proposal. |