-
-
Notifications
You must be signed in to change notification settings - Fork 38
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ import io.sentry.android.gradle.SentryTasksProvider.getAssembleTaskProvider | |
| import io.sentry.android.gradle.SentryTasksProvider.getBundleTask | ||
| import io.sentry.android.gradle.SentryTasksProvider.getMappingFileProvider | ||
| import io.sentry.android.gradle.extensions.SentryPluginExtension | ||
| import io.sentry.android.gradle.instrumentation.SentrySdkOptimizationClassVisitorFactory | ||
| import io.sentry.android.gradle.instrumentation.SpanAddingClassVisitorFactory | ||
| import io.sentry.android.gradle.services.SentryModulesService | ||
| import io.sentry.android.gradle.snapshot.GenerateSnapshotTestsTask | ||
|
|
@@ -177,35 +178,45 @@ fun ApplicationAndroidComponentsExtension.configure( | |
| } | ||
| } | ||
|
|
||
| if (extension.tracingInstrumentation.enabled.get()) { | ||
| /** | ||
| * We detect sentry-android SDK version using configurations.incoming.afterResolve. This is | ||
| * guaranteed to be executed BEFORE any of the build tasks/transforms are started. | ||
| * | ||
| * After detecting the sdk state, we use Gradle's shared build service to persist the state | ||
| * between builds and also during a single build, because transforms are run in parallel. | ||
| */ | ||
| val sentryModulesService = | ||
| val sdkOptimizationEnabled = extension.sdkOptimization.enabled.get() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. like above, i think we should rename this to |
||
| val tracingInstrumentationEnabled = extension.tracingInstrumentation.enabled.get() | ||
| // Both visitor factories need the resolved dependency graph. | ||
| val modulesService = | ||
| if (sdkOptimizationEnabled || tracingInstrumentationEnabled) { | ||
| SentryModulesService.register( | ||
| project, | ||
| extension.tracingInstrumentation.features, | ||
| extension.tracingInstrumentation.logcat.enabled, | ||
| extension.includeSourceContext, | ||
| extension.dexguardEnabled, | ||
| extension.tracingInstrumentation.appStart.enabled, | ||
| ) | ||
| /** | ||
| * We have to register SentryModulesService as a build event listener, so it will not be | ||
| * discarded after the configuration phase (where we store the collected dependencies), and | ||
| * will be passed down to the InstrumentationFactory | ||
| */ | ||
| buildEvents.onTaskCompletion(sentryModulesService) | ||
|
|
||
| project.collectModules( | ||
| "${variant.name}RuntimeClasspath", | ||
| variant.name, | ||
| sentryModulesService, | ||
| project, | ||
| extension.tracingInstrumentation.features, | ||
| extension.tracingInstrumentation.logcat.enabled, | ||
| extension.includeSourceContext, | ||
| extension.dexguardEnabled, | ||
| extension.tracingInstrumentation.appStart.enabled, | ||
| ) | ||
| .also { | ||
| // Keep the service alive after configuration so instrumentation can read it. | ||
| buildEvents.onTaskCompletion(it) | ||
| } | ||
| } else { | ||
| null | ||
| } | ||
|
|
||
| if (modulesService != null) { | ||
| project.collectModules("${variant.name}RuntimeClasspath", variant.name, modulesService) | ||
| } | ||
|
|
||
| if (sdkOptimizationEnabled) { | ||
| variant.instrumentation.transformClassesWith( | ||
| SentrySdkOptimizationClassVisitorFactory::class.java, | ||
| InstrumentationScope.ALL, | ||
| ) { params -> | ||
| params.sentryModulesService.setDisallowChanges(checkNotNull(modulesService)) | ||
| } | ||
| variant.instrumentation.setAsmFramesComputationMode( | ||
| FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS | ||
| ) | ||
| } | ||
|
|
||
| if (tracingInstrumentationEnabled) { | ||
| val tracingModulesService = checkNotNull(modulesService) | ||
|
|
||
| variant.configureInstrumentation( | ||
| SpanAddingClassVisitorFactory::class.java, | ||
|
|
@@ -219,7 +230,7 @@ fun ApplicationAndroidComponentsExtension.configure( | |
| params.debug.setDisallowChanges(extension.tracingInstrumentation.debug.get()) | ||
| params.logcatMinLevel.setDisallowChanges(extension.tracingInstrumentation.logcat.minLevel) | ||
|
|
||
| params.sentryModulesService.setDisallowChanges(sentryModulesService) | ||
| params.sentryModulesService.setDisallowChanges(tracingModulesService) | ||
| params.features.setDisallowChanges(extension.tracingInstrumentation.features) | ||
| params.logcatEnabled.setDisallowChanges(extension.tracingInstrumentation.logcat.enabled) | ||
| params.appStartEnabled.setDisallowChanges( | ||
|
|
@@ -235,7 +246,7 @@ fun ApplicationAndroidComponentsExtension.configure( | |
| project, | ||
| extension, | ||
| sentryTelemetryProvider, | ||
| sentryModulesService, | ||
| tracingModulesService, | ||
| variant.name, | ||
| ) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,10 @@ | ||||||
| package io.sentry.android.gradle.extensions | ||||||
|
|
||||||
| import javax.inject.Inject | ||||||
| import org.gradle.api.model.ObjectFactory | ||||||
| import org.gradle.api.provider.Property | ||||||
|
|
||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we name this |
||||||
| open class SdkOptimizationExtension @Inject constructor(objects: ObjectFactory) { | ||||||
| /** Enables build-time optimizations of the Sentry SDK. Defaults to true. */ | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| val enabled: Property<Boolean> = objects.property(Boolean::class.java).convention(true) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aren't we going to have different optimizations? like properties, manifest and this one? Will they all be gated behind the same flag? |
||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| package io.sentry.android.gradle.instrumentation | ||
|
|
||
| import org.objectweb.asm.ClassVisitor | ||
| import org.objectweb.asm.FieldVisitor | ||
| import org.objectweb.asm.MethodVisitor | ||
| import org.objectweb.asm.Opcodes | ||
|
|
||
| internal class LoadClassClassVisitor( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a small javadoc on this one would be helpful |
||
| 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) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun visitEnd() { | ||
| 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" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| 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.services.SentryModulesService | ||
| 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.Property | ||
| import org.gradle.api.tasks.Internal | ||
| import org.objectweb.asm.ClassVisitor | ||
|
|
||
| abstract class SentrySdkOptimizationClassVisitorFactory : | ||
| AsmClassVisitorFactory<SentrySdkOptimizationClassVisitorFactory.SdkOptimizationParameters> { | ||
|
|
||
| interface SdkOptimizationParameters : InstrumentationParameters { | ||
| @get:Internal val sentryModulesService: Property<SentryModulesService> | ||
| } | ||
|
|
||
| override fun createClassVisitor( | ||
| classContext: ClassContext, | ||
| nextClassVisitor: ClassVisitor, | ||
| ): ClassVisitor { | ||
| val service = parameters.get().sentryModulesService.get() | ||
| val modules = service.sentryModules.keys + service.externalModules.keys | ||
| return LoadClassClassVisitor( | ||
| instrumentationContext.apiVersion.get(), | ||
| nextClassVisitor, | ||
| resolveClassAvailability(modules), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Variants share mutable module stateMedium Severity All variants register and write into one Additional Locations (1)Reviewed by Cursor Bugbot for commit 94d96e4. Configure here. |
||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale availability after dependency changesHigh Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit 94d96e4. Configure here. |
||
| } | ||
|
|
||
| override fun isInstrumentable(classData: ClassData): Boolean = | ||
| classData.className == LOAD_CLASS_NAME | ||
|
|
||
| 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 } | ||
| } | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good to delete this. this is false, build services are not kept across builds.