Cache delegate recipes per recipe instance to avoid repeated construction#184
Merged
Conversation
…tion The Gradle/Maven dependency wrapper recipes (`AddDependency`, `ChangeDependency`, `UpgradeDependencyVersion`, `UpgradeTransitiveDependencyVersion`) each delegate to a Gradle and a Maven `ScanningRecipe`. Previously a fresh delegate instance was constructed on every `getInitialValue`/`getScanner`/`getVisitor`/`validate` call (and, in `AddDependency`, once per source file inside the scanner's `visit`). Each `ScanningRecipe` construction runs `UUID.randomUUID()` for its accumulator-message key, which draws from `SecureRandom` and dominated profiles of recipe-heavy runs. Memoize each delegate lazily on the wrapper recipe instance (a `final transient AtomicReference` that is excluded from the generated constructor and from `equals`/`hashCode`) so it is built at most once per wrapper instance and reused across the scanning and editing phases. The delegate is derived from the wrapper's options, so it belongs to the recipe instance rather than the accumulator: callers such as `rewrite-spring` deliberately reuse a single accumulator across two wrapper instances that have different options, so caching the delegate on the accumulator would reuse the wrong options. The `Accumulator` types are left unchanged, preserving their public constructors for downstream callers that build them directly.
2b2ed4f to
fe3f779
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The Gradle/Maven dependency wrapper recipes —
AddDependency,ChangeDependency,UpgradeDependencyVersion, andUpgradeTransitiveDependencyVersion— each delegate to a Gradle and a MavenScanningRecipe. Until now they constructed a fresh delegate instance on everygetInitialValue,getScanner,getVisitor, andvalidatecall, andAddDependency.getScannereven constructed its delegates once per source file from inside the scanner'svisit. EveryScanningRecipeconstructor computes"org.openrewrite.recipe.acc." + UUID.randomUUID()for its accumulator-message key, andUUID.randomUUID()draws fromSecureRandom. In profiles of recipe-heavy runs thisSecureRandomdraw dominated, with these wrapper recipes accounting for nearly all of the cost.The fix is to build each delegate at most once and reuse it. The delegate recipe is derived from the wrapper's options, so it logically belongs to the wrapper recipe instance, not to the run-scoped accumulator. Each wrapper memoizes its two delegates lazily in a
final transient AtomicReference—finaland initialized so it is excluded from the Lombok-generated constructor,transientso it stays out ofequals/hashCode, andAtomicReferencefor safe publication under concurrentgetScanner/getVisitorcalls. TheAccumulatortypes are deliberately left unchanged so their public constructors keep working for downstream callers that build them directly (for examplerewrite-spring, which constructsUpgradeDependencyVersion.Accumulatoritself).Caching on the instance rather than the accumulator also avoids a subtle correctness trap:
rewrite-springreuses a single accumulator across two differentUpgradeDependencyVersioninstances — an empty-option one for scanning and a real-option one for editing — so a delegate cached on the accumulator would apply the wrong options during the edit phase. Instance memoization gives each wrapper its own correctly-configured delegate.Summary
ScanningRecipelazily on the recipe instance via afinal transient AtomicReference, reused across the scanning and editing phases.AddDependency, per source file)" to at most one Gradle and one Maven delegate per wrapper instance.AddDependency.getScanner's delegate-scanner creation out of the per-source-filevisit.Accumulatortypes are unchanged, preserving their public constructors.Test plan
AddDependencyTest,ChangeDependencyTest, andUpgradeDependencyVersionTest— green before and after.rewrite-spring, which constructsUpgradeDependencyVersion.Accumulatordirectly — compiles successfully, confirming the public accumulator API is preserved.