From c485c20c1fbf6ecab24919c5f641695bf213c08d Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Jul 2026 14:17:44 +0200 Subject: [PATCH 1/3] perf(instrumentation): Resolve SDK class availability at build time Add an independently configurable ASM visitor that derives known optional classes from resolved modules and injects their availability into LoadClass. Avoid repeated reflection during SDK startup. Refs JAVA-654 Co-Authored-By: Codex --- .../android/gradle/AndroidComponentsConfig.kt | 69 +++++---- .../extensions/SdkOptimizationExtension.kt | 10 ++ .../extensions/SentryPluginExtension.kt | 8 + .../instrumentation/LoadClassClassVisitor.kt | 117 +++++++++++++++ ...entrySdkOptimizationClassVisitorFactory.kt | 75 ++++++++++ .../io/sentry/android/gradle/util/Versions.kt | 2 + .../LoadClassClassVisitorTest.kt | 141 ++++++++++++++++++ .../SentryPluginCheckAndroidSdkTest.kt | 3 +- .../gradle/integration/SentryPluginTest.kt | 21 ++- 9 files changed, 415 insertions(+), 31 deletions(-) create mode 100644 plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SdkOptimizationExtension.kt create mode 100644 plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt create mode 100644 plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt create mode 100644 plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitorTest.kt diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt index 32c7123ca..9efae003c 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/AndroidComponentsConfig.kt @@ -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() + 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, ) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SdkOptimizationExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SdkOptimizationExtension.kt new file mode 100644 index 000000000..20dab9f20 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SdkOptimizationExtension.kt @@ -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 + +open class SdkOptimizationExtension @Inject constructor(objects: ObjectFactory) { + /** Enables build-time optimizations of the Sentry SDK. Defaults to true. */ + val enabled: Property = objects.property(Boolean::class.java).convention(true) +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt index 7cd7daf87..5e8fa38de 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/extensions/SentryPluginExtension.kt @@ -97,6 +97,14 @@ abstract class SentryPluginExtension @Inject constructor(objects: ObjectFactory) tracingInstrumentationAction.execute(tracingInstrumentation) } + val sdkOptimization: SdkOptimizationExtension = + objects.newInstance(SdkOptimizationExtension::class.java) + + /** Configure build-time optimizations of the Sentry SDK. Default configuration is enabled. */ + fun sdkOptimization(sdkOptimizationAction: Action) { + sdkOptimizationAction.execute(sdkOptimization) + } + val autoInstallation: AutoInstallExtension = objects.newInstance(AutoInstallExtension::class.java) /** Configure the auto installation feature. */ diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt new file mode 100644 index 000000000..ea1945f29 --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt @@ -0,0 +1,117 @@ +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( + apiVersion: Int, + nextClassVisitor: ClassVisitor, + private val classAvailability: Map, +) : 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?, + ): 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) + visitor.visitMaxs(3, 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, "", "()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 = "" + const val VOID_METHOD_DESCRIPTOR = "()V" + } +} diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt new file mode 100644 index 000000000..10b662c8d --- /dev/null +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/SentrySdkOptimizationClassVisitorFactory.kt @@ -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 { + + interface SdkOptimizationParameters : InstrumentationParameters { + @get:Internal val sentryModulesService: Property + } + + 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), + ) + } + + 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> = + 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): Map = + SentrySdkOptimizationClassVisitorFactory.CLASS_MODULES.mapValues { (_, owners) -> + owners.any { it in modules } + } diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt index c57c774b6..dcc480c02 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/util/Versions.kt @@ -56,6 +56,8 @@ internal object SentryModules { DefaultModuleIdentifier.newId("io.sentry", "sentry-android-navigation") internal val SENTRY_ANDROID_TIMBER = DefaultModuleIdentifier.newId("io.sentry", "sentry-android-timber") + internal val SENTRY_ANDROID_REPLAY = + DefaultModuleIdentifier.newId("io.sentry", "sentry-android-replay") internal val SENTRY_ANDROID_DISTRIBUTION = DefaultModuleIdentifier.newId("io.sentry", "sentry-android-distribution") internal val SENTRY_OKHTTP = DefaultModuleIdentifier.newId("io.sentry", "sentry-okhttp") diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitorTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitorTest.kt new file mode 100644 index 000000000..723192d31 --- /dev/null +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitorTest.kt @@ -0,0 +1,141 @@ +package io.sentry.android.gradle.instrumentation + +import com.google.common.truth.Truth.assertThat +import io.sentry.android.gradle.util.SentryModules +import org.gradle.api.internal.artifacts.DefaultModuleIdentifier +import org.junit.Test +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes + +class LoadClassClassVisitorTest { + @Test + fun `resolves every known class from the module graph`() { + val modules = SentrySdkOptimizationClassVisitorFactory.CLASS_MODULES.values.flatten().toSet() + + assertThat(resolveClassAvailability(modules).values).doesNotContain(false) + } + + @Test + fun `marks every known class as unavailable when its module is absent`() { + val availability = resolveClassAvailability(emptySet()) + + assertThat(availability).hasSize(SentrySdkOptimizationClassVisitorFactory.CLASS_MODULES.size) + assertThat(availability.values).doesNotContain(true) + } + + @Test + fun `supports platform-specific module coordinates`() { + val modules = + setOf( + DefaultModuleIdentifier.newId("androidx.compose.ui", "ui-android"), + DefaultModuleIdentifier.newId("androidx.lifecycle", "lifecycle-common-jvm"), + SentryModules.SENTRY_ANDROID_REPLAY, + ) + + assertThat(resolveClassAvailability(modules)) + .containsAtLeast( + "androidx.compose.ui.node.Owner", + true, + "androidx.lifecycle.Lifecycle", + true, + "io.sentry.android.replay.ReplayIntegration", + true, + ) + } + + @Test + fun `injects availability map when static initializer is missing`() { + val availability = mapOf("present.Class" to true, "missing.Class" to false) + + val clazz = load(transformClass(hasStaticInitializer = false, availability = availability)) + + assertThat(readAvailability(clazz)).containsExactlyEntriesIn(availability) + } + + @Test + fun `injects availability map and preserves existing static initializer`() { + val availability = mapOf("present.Class" to true) + + val clazz = load(transformClass(hasStaticInitializer = true, availability = availability)) + + assertThat(clazz.getDeclaredField("marker").getInt(null)).isEqualTo(7) + assertThat(readAvailability(clazz)).containsExactlyEntriesIn(availability) + } + + @Test + fun `does not modify SDK versions without the availability field`() { + val clazz = load(transformClass(hasAvailabilityField = false)) + + assertThat(clazz.declaredFields.map { it.name }).doesNotContain("classAvailability") + } + + private fun transformClass( + hasAvailabilityField: Boolean = true, + hasStaticInitializer: Boolean = false, + availability: Map = emptyMap(), + ): ByteArray { + val original = ClassWriter(0) + original.visit( + Opcodes.V1_8, + Opcodes.ACC_PUBLIC, + LOAD_CLASS_INTERNAL_NAME, + null, + "java/lang/Object", + null, + ) + if (hasAvailabilityField) { + original + .visitField( + Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC, + "classAvailability", + "Ljava/util/Map;", + null, + null, + ) + .visitEnd() + } + if (hasStaticInitializer) { + original + .visitField(Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC, "marker", "I", null, null) + .visitEnd() + original.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null).apply { + visitCode() + visitIntInsn(Opcodes.BIPUSH, 7) + visitFieldInsn(Opcodes.PUTSTATIC, LOAD_CLASS_INTERNAL_NAME, "marker", "I") + visitInsn(Opcodes.RETURN) + visitMaxs(1, 0) + visitEnd() + } + } + original.visitEnd() + + val reader = ClassReader(original.toByteArray()) + val writer = ClassWriter(reader, ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) + reader.accept(LoadClassClassVisitor(Opcodes.ASM9, writer, availability), 0) + return writer.toByteArray() + } + + private fun load(bytes: ByteArray): Class<*> = + object : ClassLoader(javaClass.classLoader) { + fun define(): Class<*> = + defineClass( + SentrySdkOptimizationClassVisitorFactory.LOAD_CLASS_NAME, + bytes, + 0, + bytes.size, + ) + } + .define() + + @Suppress("UNCHECKED_CAST") + private fun readAvailability(clazz: Class<*>): Map = + clazz.getDeclaredField("classAvailability").run { + isAccessible = true + get(null) as Map + } + + private companion object { + const val LOAD_CLASS_INTERNAL_NAME = "io/sentry/util/LoadClass" + } +} diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt index 4417328be..7bbc33f70 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginCheckAndroidSdkTest.kt @@ -9,11 +9,12 @@ class SentryPluginCheckAndroidSdkTest : BaseSentryPluginTest(BuildConfig.AgpVersion, GradleVersion.current().version) { @Test - fun `when tracingInstrumentation is disabled does not check sentry-android sdk state`() { + fun `when instrumentation is disabled does not check sentry-android sdk state`() { appBuildFile.appendText( // language=Groovy """ sentry.tracingInstrumentation.enabled = false + sentry.sdkOptimization.enabled = false ${captureSdkState()} """ diff --git a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt index fb444b2bc..c837c4b78 100644 --- a/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt +++ b/plugin-build/src/test/kotlin/io/sentry/android/gradle/integration/SentryPluginTest.kt @@ -450,13 +450,28 @@ class SentryPluginTest : @Test fun `skips tracing instrumentation if tracingInstrumentation is disabled`() { - applyTracingInstrumentation(false, appStart = false, logcat = false) + applyTracingInstrumentation( + tracingInstrumentation = false, + sdkOptimization = false, + appStart = false, + logcat = false, + ) val build = runner.appendArguments(":app:assembleRelease", "--dry-run").build() assertFalse(":app:transformReleaseClassesWithAsm" in build.output) } + @Test + fun `registers sdk optimization independently from tracing instrumentation`() { + applyTracingInstrumentation(false, appStart = false, logcat = false) + + val build = runner.appendArguments(":app:assembleRelease", "--info").build() + + assertTrue(":app:transformReleaseClassesWithAsm" in build.output) + assertTrue("Detected Sentry modules" in build.output) + } + @Test fun `register tracing instrumentation if tracingInstrumentation is enabled`() { applyTracingInstrumentation() @@ -1176,6 +1191,7 @@ class SentryPluginTest : private fun applyTracingInstrumentation( tracingInstrumentation: Boolean = true, + sdkOptimization: Boolean = true, features: Set = emptySet(), logcat: Boolean = false, appStart: Boolean = false, @@ -1196,6 +1212,9 @@ class SentryPluginTest : sentry { autoUploadProguardMapping = false + sdkOptimization { + enabled = $sdkOptimization + } tracingInstrumentation { forceInstrumentDependencies = $forceInstrumentDependencies enabled = $tracingInstrumentation From e02ab5f80d67839a91f079a4fc355ad261306f92 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Jul 2026 14:28:20 +0200 Subject: [PATCH 2/3] docs(changelog): Add SDK optimization entry Document the build-time SDK class availability optimization introduced in #1375. Refs JAVA-654 Co-Authored-By: Codex --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 141c7a20d..a8e673ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - This breaking change is only for customers using self-hosted Sentry together with a user auth token (not an org auth token) and the url and auth token are configured separately: - If so, this breaking change applies to you in order to patch a security flaw. [Please read this](https://github.com/getsentry/sentry-cli/issues/3380#issuecomment-5059013026) for further details. +### Features + +- Resolve optional Sentry SDK class availability at build time to reduce SDK initialization overhead ([#1375](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1375)) + ### Dependencies - Bump CLI from v3.6.1 to v3.6.2 ([#1373](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1373)) From 94d96e4d9a8867e9ef9e42b6871e0b17026b51f2 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 30 Jul 2026 16:33:03 +0200 Subject: [PATCH 3/3] ref(instrumentation): Defer max computation to ASM Use zero placeholders and document that visitMaxs triggers frame and max computation. Refs LINEAR-JAVA-654 Co-Authored-By: Codex --- .../android/gradle/instrumentation/LoadClassClassVisitor.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt index ea1945f29..ba7687476 100644 --- a/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt +++ b/plugin-build/src/main/kotlin/io/sentry/android/gradle/instrumentation/LoadClassClassVisitor.kt @@ -62,7 +62,8 @@ internal class LoadClassClassVisitor( visitor.visitCode() injectClassAvailability(visitor) visitor.visitInsn(Opcodes.RETURN) - visitor.visitMaxs(3, 0) + // Triggers ASM frame/max computation; arguments are ignored in compute mode. + visitor.visitMaxs(0, 0) visitor.visitEnd() } super.visitEnd()