-
-
Notifications
You must be signed in to change notification settings - Fork 41
perf(instrumentation): Resolve SDK class availability at build time #1375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
ad685a1
perf(instrumentation): Resolve SDK class availability at build time
romtsn 632c774
docs(changelog): Add SDK optimization entry
romtsn 13cc92a
ref(instrumentation): Defer max computation to ASM
romtsn 960d72d
docs(instrumentation): Explain class availability injection
romtsn cfac2fa
fix(instrumentation): Track class availability per variant
romtsn 7defbeb
ref(instrumentation): Rename runtime optimization DSL
romtsn fa2580f
chore(instrumentation): Log skipped reflection optimization
romtsn 59b2550
docs(changelog): Expand runtime optimization entry
romtsn c65f626
docs(changelog): Move optimization entry to Unreleased
romtsn 5a9e232
docs(changelog): Simplify optimization benchmark details
romtsn 439374f
fix(instrumentation): Preserve fallback without classpath
romtsn b72ad39
fix(instrumentation): Scope classpath fallback to LoadClass
romtsn c36cc3d
docs(instrumentation): Explain LoadClass fallback
romtsn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
12 changes: 12 additions & 0 deletions
12
...uild/src/main/kotlin/io/sentry/android/gradle/extensions/RuntimeOptimizationsExtension.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package io.sentry.android.gradle.extensions | ||
|
|
||
| import javax.inject.Inject | ||
| import org.gradle.api.model.ObjectFactory | ||
| import org.gradle.api.provider.Property | ||
|
|
||
|
romtsn marked this conversation as resolved.
|
||
| open class RuntimeOptimizationsExtension @Inject constructor(objects: ObjectFactory) { | ||
| /** | ||
| * Enables runtime optimizations of the Sentry SDK at the cost of build time. Defaults to true. | ||
| */ | ||
| val enabled: Property<Boolean> = objects.property(Boolean::class.java).convention(true) | ||
|
romtsn marked this conversation as resolved.
|
||
| } | ||
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
142 changes: 142 additions & 0 deletions
142
...n-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| package io.sentry.android.gradle.instrumentation | ||
|
|
||
| import io.sentry.android.gradle.SentryPlugin | ||
| import org.objectweb.asm.ClassVisitor | ||
| import org.objectweb.asm.FieldVisitor | ||
| import org.objectweb.asm.MethodVisitor | ||
| import org.objectweb.asm.Opcodes | ||
|
|
||
| /** | ||
| * Injects build-time knowledge of optional classes into `LoadClass`. | ||
| * | ||
| * `classAvailability` maps class names probed during SDK initialization to whether their owning | ||
| * dependency is present. Before instrumentation the field is uninitialized: | ||
| * ```java | ||
| * static Map<String, Boolean> classAvailability; | ||
| * ``` | ||
| * | ||
| * After instrumentation the generated static initializer is equivalent to: | ||
| * ```java | ||
| * classAvailability = new HashMap<>(); | ||
| * classAvailability.put("timber.log.Timber", true); | ||
| * ``` | ||
| * | ||
| * Known entries avoid reflection; omitted class names still fall back to it. SDK versions without | ||
| * the field are left unchanged. | ||
| */ | ||
| internal class LoadClassClassVisitor( | ||
|
romtsn marked this conversation as resolved.
|
||
| apiVersion: Int, | ||
| nextClassVisitor: ClassVisitor, | ||
| private val classAvailability: Map<String, Boolean>, | ||
| ) : ClassVisitor(apiVersion, nextClassVisitor) { | ||
| private var hasAvailabilityField = false | ||
| private var hasStaticInitializer = false | ||
|
|
||
| override fun visitField( | ||
| access: Int, | ||
| name: String?, | ||
| descriptor: String?, | ||
| signature: String?, | ||
| value: Any?, | ||
| ): FieldVisitor? { | ||
| if (name == AVAILABILITY_FIELD && descriptor == MAP_DESCRIPTOR) { | ||
| hasAvailabilityField = true | ||
| } | ||
| return super.visitField(access, name, descriptor, signature, value) | ||
| } | ||
|
|
||
| override fun visitMethod( | ||
| access: Int, | ||
| name: String?, | ||
| descriptor: String?, | ||
| signature: String?, | ||
| exceptions: Array<out String>?, | ||
| ): MethodVisitor { | ||
| val visitor = super.visitMethod(access, name, descriptor, signature, exceptions) | ||
| if (name != STATIC_INITIALIZER || descriptor != VOID_METHOD_DESCRIPTOR) { | ||
| return visitor | ||
| } | ||
|
|
||
| hasStaticInitializer = true | ||
| return object : MethodVisitor(api, visitor) { | ||
| override fun visitInsn(opcode: Int) { | ||
| if (opcode == Opcodes.RETURN && hasAvailabilityField) { | ||
| injectClassAvailability(this) | ||
| } | ||
| super.visitInsn(opcode) | ||
|
sentry[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun visitEnd() { | ||
| if (!hasAvailabilityField) { | ||
| SentryPlugin.logger.info( | ||
| "Sentry SDK runtime reflection checks were not optimized because the current SDK version does not support this optimization." | ||
| ) | ||
| } | ||
| if (hasAvailabilityField && !hasStaticInitializer) { | ||
| val visitor = | ||
| super.visitMethod( | ||
| Opcodes.ACC_STATIC, | ||
| STATIC_INITIALIZER, | ||
| VOID_METHOD_DESCRIPTOR, | ||
| null, | ||
| null, | ||
| ) | ||
| visitor.visitCode() | ||
| injectClassAvailability(visitor) | ||
| visitor.visitInsn(Opcodes.RETURN) | ||
| // Triggers ASM frame/max computation; arguments are ignored in compute mode. | ||
| visitor.visitMaxs(0, 0) | ||
| visitor.visitEnd() | ||
| } | ||
| super.visitEnd() | ||
| } | ||
|
|
||
| private fun injectClassAvailability(visitor: MethodVisitor) { | ||
| visitor.visitTypeInsn(Opcodes.NEW, HASH_MAP_NAME) | ||
| visitor.visitInsn(Opcodes.DUP) | ||
| visitor.visitMethodInsn(Opcodes.INVOKESPECIAL, HASH_MAP_NAME, "<init>", "()V", false) | ||
| visitor.visitFieldInsn( | ||
| Opcodes.PUTSTATIC, | ||
| LOAD_CLASS_INTERNAL_NAME, | ||
| AVAILABILITY_FIELD, | ||
| MAP_DESCRIPTOR, | ||
| ) | ||
|
|
||
| classAvailability.forEach { (className, available) -> | ||
| visitor.visitFieldInsn( | ||
| Opcodes.GETSTATIC, | ||
| LOAD_CLASS_INTERNAL_NAME, | ||
| AVAILABILITY_FIELD, | ||
| MAP_DESCRIPTOR, | ||
| ) | ||
| visitor.visitLdcInsn(className) | ||
| visitor.visitInsn(if (available) Opcodes.ICONST_1 else Opcodes.ICONST_0) | ||
| visitor.visitMethodInsn( | ||
| Opcodes.INVOKESTATIC, | ||
| "java/lang/Boolean", | ||
| "valueOf", | ||
| "(Z)Ljava/lang/Boolean;", | ||
| false, | ||
| ) | ||
| visitor.visitMethodInsn( | ||
| Opcodes.INVOKEINTERFACE, | ||
| "java/util/Map", | ||
| "put", | ||
| "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", | ||
| true, | ||
| ) | ||
| visitor.visitInsn(Opcodes.POP) | ||
| } | ||
| } | ||
|
|
||
| private companion object { | ||
| const val LOAD_CLASS_INTERNAL_NAME = "io/sentry/util/LoadClass" | ||
| const val AVAILABILITY_FIELD = "classAvailability" | ||
| const val MAP_DESCRIPTOR = "Ljava/util/Map;" | ||
| const val HASH_MAP_NAME = "java/util/HashMap" | ||
| const val STATIC_INITIALIZER = "<clinit>" | ||
| const val VOID_METHOD_DESCRIPTOR = "()V" | ||
| } | ||
| } | ||
74 changes: 74 additions & 0 deletions
74
...tlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package io.sentry.android.gradle.instrumentation | ||
|
|
||
| import com.android.build.api.instrumentation.AsmClassVisitorFactory | ||
| import com.android.build.api.instrumentation.ClassContext | ||
| import com.android.build.api.instrumentation.ClassData | ||
| import com.android.build.api.instrumentation.InstrumentationParameters | ||
| import io.sentry.android.gradle.util.SentryModules | ||
| import org.gradle.api.artifacts.ModuleIdentifier | ||
| import org.gradle.api.internal.artifacts.DefaultModuleIdentifier | ||
| import org.gradle.api.provider.MapProperty | ||
| import org.gradle.api.tasks.Input | ||
| import org.objectweb.asm.ClassVisitor | ||
|
|
||
| abstract class SentrySdkOptimizationClassVisitorFactory : | ||
| AsmClassVisitorFactory<SentrySdkOptimizationClassVisitorFactory.SdkOptimizationParameters> { | ||
|
|
||
| interface SdkOptimizationParameters : InstrumentationParameters { | ||
| @get:Input val classAvailability: MapProperty<String, Boolean> | ||
| } | ||
|
|
||
| override fun createClassVisitor( | ||
| classContext: ClassContext, | ||
| nextClassVisitor: ClassVisitor, | ||
| ): ClassVisitor { | ||
| return LoadClassClassVisitor( | ||
| instrumentationContext.apiVersion.get(), | ||
| nextClassVisitor, | ||
| parameters.get().classAvailability.get(), | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // Empty availability means the runtime classpath is unknown. Skip this transformation so | ||
| // LoadClass falls back to reflection. | ||
| override fun isInstrumentable(classData: ClassData): Boolean = | ||
| classData.className == LOAD_CLASS_NAME && parameters.get().classAvailability.get().isNotEmpty() | ||
|
romtsn marked this conversation as resolved.
|
||
|
|
||
| internal companion object { | ||
| const val LOAD_CLASS_NAME = "io.sentry.util.LoadClass" | ||
|
|
||
| val CLASS_MODULES: Map<String, Set<ModuleIdentifier>> = | ||
| sortedMapOf( | ||
| "androidx.compose.ui.node.Owner" to | ||
| setOf(module("androidx.compose.ui", "ui"), module("androidx.compose.ui", "ui-android")), | ||
| "androidx.core.view.ScrollingView" to setOf(module("androidx.core", "core")), | ||
| "androidx.fragment.app.FragmentManager\$FragmentLifecycleCallbacks" to | ||
| setOf(module("androidx.fragment", "fragment")), | ||
| "androidx.lifecycle.Lifecycle" to | ||
| setOf( | ||
| module("androidx.lifecycle", "lifecycle-common"), | ||
| module("androidx.lifecycle", "lifecycle-common-jvm"), | ||
| ), | ||
| "io.sentry.android.distribution.DistributionIntegration" to | ||
| setOf(SentryModules.SENTRY_ANDROID_DISTRIBUTION), | ||
| "io.sentry.android.fragment.FragmentLifecycleIntegration" to | ||
| setOf(SentryModules.SENTRY_ANDROID_FRAGMENT), | ||
| "io.sentry.android.replay.ReplayIntegration" to setOf(SentryModules.SENTRY_ANDROID_REPLAY), | ||
| "io.sentry.android.timber.SentryTimberIntegration" to | ||
| setOf(SentryModules.SENTRY_ANDROID_TIMBER), | ||
| "io.sentry.compose.gestures.ComposeGestureTargetLocator" to | ||
| setOf(SentryModules.SENTRY_ANDROID_COMPOSE, module("io.sentry", "sentry-compose")), | ||
| "io.sentry.compose.viewhierarchy.ComposeViewHierarchyExporter" to | ||
| setOf(SentryModules.SENTRY_ANDROID_COMPOSE, module("io.sentry", "sentry-compose")), | ||
| "timber.log.Timber" to setOf(module("com.jakewharton.timber", "timber")), | ||
| ) | ||
|
|
||
| private fun module(group: String, name: String): ModuleIdentifier = | ||
| DefaultModuleIdentifier.newId(group, name) | ||
| } | ||
| } | ||
|
|
||
| internal fun resolveClassAvailability(modules: Set<ModuleIdentifier>): Map<String, Boolean> = | ||
| SentrySdkOptimizationClassVisitorFactory.CLASS_MODULES.mapValues { (_, owners) -> | ||
| owners.any { it in modules } | ||
| } | ||
|
romtsn marked this conversation as resolved.
|
||
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.