+1

Gianluca Sartori
--
https://dueuno.com

On Mon, 1 Jun 2026 at 19:15, 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:
>
>    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.
>
> 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 unless
>    those runtimes explicitly opt in.
>    7.
>
>    Solve dependency resolution caching for @Grab, although it must
>    interact safely with it.
>
> Proposed Behavior
>
> When running:
>
> groovy myscript.groovy
>
> the launcher may:
>
>    1.
>
>    Compute a cache key from the script and compilation environment.
>    2.
>
>    Look for previously generated class files matching that key.
>    3.
>
>    If found and valid, load the cached classes.
>    4.
>
>    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:
>
>    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 keys should prevent cross-user or cross-project collisions.
>    5.
>
>    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:
>
>    1.
>
>    Extend the groovy launcher with a cache-aware script runner.
>    2.
>
>    Before compilation, compute a ScriptCacheKey.
>    3.
>
>    Look for a matching cache entry.
>    4.
>
>    If present, load generated classes using an appropriate class loader.
>    5.
>
>    If absent, compile the script as today.
>    6.
>
>    Capture generated bytecode.
>    7.
>
>    Persist generated bytecode and metadata atomically.
>    8.
>
>    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:
>
>    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.
>
> 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