Re: Script caching
MG <mgbiz-yvYIh6MZAuFWk0Htik3J/[email protected]> Mon, 1 Jun 2026 22:38:25 +0200
| Newsgroups | gmane.comp.lang.groovy.devel |
|---|---|
| Message-ID | <[email protected]> |
Hi Per, that looks like a good idea & well written proposal to me G-) Cheers, mg Am 01.06.2026 um 19:15 schrieb Per Nyfelt: > > 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. > >