Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

- Support ProGuard mapping tasks when R8 is enabled with the AGP app `optimization.enable` DSL ([#1376](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1376))

### Performance

- Eliminate reflection for known optional Sentry SDK class-availability checks, reducing SDK initialization time by about 1% in an absent-heavy startup benchmark ([#1375](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1375))
- This optimization is enabled by default. If it causes problems, disable it with `sentry.runtimeOptimizations.enabled = false`.

### Dependencies

- Bump Android SDK from v8.51.0 to v8.52.0 ([#1378](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1378))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ 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.instrumentation.resolveClassAvailability
import io.sentry.android.gradle.services.SentryModulesService
import io.sentry.android.gradle.snapshot.GenerateSnapshotTestsTask
import io.sentry.android.gradle.sourcecontext.OutputPaths
Expand Down Expand Up @@ -177,35 +179,48 @@ 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
Comment thread
romtsn marked this conversation as resolved.
* between builds and also during a single build, because transforms are run in parallel.
*/
val sentryModulesService =
val runtimeOptimizationsEnabled = extension.runtimeOptimizations.enabled.get()
val tracingInstrumentationEnabled = extension.tracingInstrumentation.enabled.get()
// Both visitor factories need the resolved dependency graph.
val modulesService =
if (runtimeOptimizationsEnabled || tracingInstrumentationEnabled) {
SentryModulesService.register(
project,
extension.tracingInstrumentation.features,
extension.tracingInstrumentation.logcat.enabled,
extension.includeSourceContext,
extension.dexguardEnabled,
extension.tracingInstrumentation.appStart.enabled,
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
}

val modules =
modulesService?.let {
project.collectModules("${variant.name}RuntimeClasspath", variant.name, it)
}

if (runtimeOptimizationsEnabled) {
variant.instrumentation.transformClassesWith(
SentrySdkOptimizationClassVisitorFactory::class.java,
InstrumentationScope.ALL,
) { params ->
params.classAvailability.setDisallowChanges(
checkNotNull(modules).map(::resolveClassAvailability).orElse(emptyMap())
)
/**
* 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,
}
Comment thread
cursor[bot] marked this conversation as resolved.
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
)
}

if (tracingInstrumentationEnabled) {
val tracingModulesService = checkNotNull(modulesService)

variant.configureInstrumentation(
SpanAddingClassVisitorFactory::class.java,
Expand All @@ -219,7 +234,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(
Expand All @@ -235,7 +250,7 @@ fun ApplicationAndroidComponentsExtension.configure(
project,
extension,
sentryTelemetryProvider,
sentryModulesService,
tracingModulesService,
variant.name,
)

Expand Down
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

Comment thread
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)
Comment thread
romtsn marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ abstract class SentryPluginExtension @Inject constructor(objects: ObjectFactory)
tracingInstrumentationAction.execute(tracingInstrumentation)
}

val runtimeOptimizations: RuntimeOptimizationsExtension =
objects.newInstance(RuntimeOptimizationsExtension::class.java)

/** Configure runtime optimizations of the Sentry SDK. Default configuration is enabled. */
fun runtimeOptimizations(runtimeOptimizationsAction: Action<RuntimeOptimizationsExtension>) {
runtimeOptimizationsAction.execute(runtimeOptimizations)
}

val autoInstallation: AutoInstallExtension = objects.newInstance(AutoInstallExtension::class.java)

/** Configure the auto installation feature. */
Expand Down
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(
Comment thread
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)
Comment thread
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"
}
}
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(),
)
Comment thread
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()
Comment thread
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 }
}
Comment thread
romtsn marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ fun Project.collectModules(
configurationName: String,
variantName: String,
sentryModulesService: Provider<SentryModulesService>,
) {
): Provider<Set<ModuleIdentifier>> {
val configProvider =
try {
configurations.named(configurationName)
} catch (e: UnknownDomainObjectException) {
logger.warn { "Unable to find configuration $configurationName for variant $variantName." }
sentryModulesService.get().sentryModules = emptyMap()
sentryModulesService.get().externalModules = emptyMap()
return
return provider<Set<ModuleIdentifier>> { null }
}

configProvider.configure { configuration ->
Expand All @@ -40,6 +40,12 @@ fun Project.collectModules(
sentryModulesService.get().externalModules = externalModules
}
}

return configProvider.map { configuration ->
configuration.incoming.resolutionResult.allComponents
.mapNotNull { it.moduleVersion?.module }
.toSet()
}
}

private fun Set<ResolvedComponentResult>.versionMap(
Expand Down
Loading
Loading