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.
>
>

Reply via email to