Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Avoid resolving the runtime classpath during Gradle configuration when SDK runtime optimizations are enabled ([#1404](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1404))

### Performance

- Resolve Sentry manifest metadata at build time to reduce Android SDK initialization overhead ([#1405](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1405))

### Dependencies

- Bump ComposablePreviewScanner from v0.9.2 to v0.9.3 ([#1408](https://github.com/getsentry/sentry-android-gradle-plugin/pull/1408))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import io.sentry.android.gradle.snapshot.GenerateSnapshotTestsTask
import io.sentry.android.gradle.sourcecontext.OutputPaths
import io.sentry.android.gradle.sourcecontext.SourceContext
import io.sentry.android.gradle.tasks.GenerateDistributionPropertiesTask
import io.sentry.android.gradle.tasks.GenerateSentryBuildTimeOptionsTask
import io.sentry.android.gradle.tasks.InjectSentryMetaPropertiesIntoAssetsTask
import io.sentry.android.gradle.tasks.PropertiesFileOutputTask
import io.sentry.android.gradle.tasks.SentryGenerateIntegrationListTask
Expand All @@ -36,6 +35,7 @@ import io.sentry.android.gradle.tasks.SentryUploadProguardMappingsTask
import io.sentry.android.gradle.tasks.SentryUploadSnapshotsTask
import io.sentry.android.gradle.tasks.configureNativeSymbolsTask
import io.sentry.android.gradle.tasks.dependencies.SentryExternalDependenciesReportTaskV2
import io.sentry.android.gradle.tasks.optimization.GenerateSentryBuildTimeOptionsTask
import io.sentry.android.gradle.telemetry.SentryTelemetryService
import io.sentry.android.gradle.util.AgpVersions
import io.sentry.android.gradle.util.GroovyCompat
Expand Down Expand Up @@ -209,6 +209,8 @@ fun ApplicationAndroidComponentsExtension.configure(
project,
"${variant.name}RuntimeClasspath",
variant.name.capitalized,
variant.artifacts.get(SingleArtifact.MERGED_MANIFEST),
sentryTelemetryProvider,
)
val javaSources = variant.sources.java
if (buildTimeOptionsTask != null && javaSources != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.sentry.android.gradle

import java.io.File
import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Element

internal object ManifestMetadataParser {
fun parse(manifest: File): Map<String, String>? =
runCatching {
val document =
manifest.inputStream().buffered().use {
Comment thread
romtsn marked this conversation as resolved.
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(it)
}
val application = document.getElementsByTagName("application").item(0) ?: return emptyMap()
val metadata = linkedMapOf<String, String>()

for (index in 0 until application.childNodes.length) {
val element = application.childNodes.item(index) as? Element ?: continue
if (element.tagName != "meta-data") continue

val name = element.getAttribute("android:name")
if (!name.startsWith("io.sentry.")) continue
val value = element.getAttribute("android:value")
if (
element.hasAttribute("android:resource") ||
!element.hasAttribute("android:value") ||
value.startsWith("@") ||
value.contains("${'$'}{")
) {
SentryPlugin.logger.info(
"Sentry manifest metadata was not optimized because $name could not be resolved at build time."
)
return null
}
metadata[name] = value
}
metadata
}
.onFailure {
SentryPlugin.logger.info(
"Sentry manifest metadata could not be parsed for optimization.",
it,
)
}
.getOrNull()

internal fun inferType(value: String): Any =
when (value) {
"true" -> true
"false" -> false
else -> parseInteger(value) ?: value.toFloatOrNull()?.takeIf { it.isFinite() } ?: value
}

private fun parseInteger(value: String): Int? =
if (value.matches(DECIMAL_INTEGER)) {
value.toIntOrNull()
} else if (value.matches(HEX_INTEGER)) {
value.removePrefix("+").let { Integer.decode(it) }
Comment thread
romtsn marked this conversation as resolved.
} else {
null
}

private val DECIMAL_INTEGER = Regex("[+-]?\\d+")
private val HEX_INTEGER = Regex("[+-]?0[xX][0-9a-fA-F]+")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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 app-owned manifest metadata into the SDK's manifest metadata reader. */
internal class ManifestMetadataClassVisitor(apiVersion: Int, nextClassVisitor: ClassVisitor) :
ClassVisitor(apiVersion, nextClassVisitor) {
private var hasMetadataField = false
private var hasStaticInitializer = false

override fun visitField(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
value: Any?,
): FieldVisitor? {
if (
name == METADATA_FIELD && descriptor == MAP_DESCRIPTOR && access and Opcodes.ACC_STATIC != 0
) {
hasMetadataField = 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 && hasMetadataField) injectMetadata(this)
super.visitInsn(opcode)
}
}
}

override fun visitEnd() {
if (!hasMetadataField) {
SentryPlugin.logger.info(
"Sentry manifest metadata was not optimized because the current SDK version does not support this optimization."
)
}
if (hasMetadataField && !hasStaticInitializer) {
val visitor =
super.visitMethod(
Opcodes.ACC_STATIC,
STATIC_INITIALIZER,
VOID_METHOD_DESCRIPTOR,
null,
null,
)
visitor.visitCode()
injectMetadata(visitor)
visitor.visitInsn(Opcodes.RETURN)
visitor.visitMaxs(0, 0)
visitor.visitEnd()
}
super.visitEnd()
}

private fun injectMetadata(visitor: MethodVisitor) {
visitor.visitMethodInsn(
Opcodes.INVOKESTATIC,
GENERATED_OPTIONS_INTERNAL_NAME,
"getManifestMetadata",
"()Ljava/util/Map;",
false,
)
visitor.visitFieldInsn(
Opcodes.PUTSTATIC,
MANIFEST_METADATA_READER_INTERNAL_NAME,
METADATA_FIELD,
MAP_DESCRIPTOR,
)
}

private companion object {
const val MANIFEST_METADATA_READER_INTERNAL_NAME =
"io/sentry/android/core/ManifestMetadataReader"
const val GENERATED_OPTIONS_INTERNAL_NAME =
"io/sentry/android/core/SentryGeneratedBuildTimeOptions"
const val METADATA_FIELD = "manifestMetadata"
const val MAP_DESCRIPTOR = "Ljava/util/Map;"
const val STATIC_INITIALIZER = "<clinit>"
const val VOID_METHOD_DESCRIPTOR = "()V"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@ abstract class SentrySdkOptimizationClassVisitorFactory :
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor,
): ClassVisitor {
return LoadClassClassVisitor(instrumentationContext.apiVersion.get(), nextClassVisitor)
}
): ClassVisitor =
when (classContext.currentClassData.className) {
LOAD_CLASS_NAME ->
LoadClassClassVisitor(instrumentationContext.apiVersion.get(), nextClassVisitor)
MANIFEST_METADATA_READER_NAME ->
ManifestMetadataClassVisitor(instrumentationContext.apiVersion.get(), nextClassVisitor)
else -> nextClassVisitor
}

override fun isInstrumentable(classData: ClassData): Boolean =
classData.className == LOAD_CLASS_NAME
classData.className == LOAD_CLASS_NAME || classData.className == MANIFEST_METADATA_READER_NAME

internal companion object {
const val LOAD_CLASS_NAME = "io.sentry.util.LoadClass"
const val MANIFEST_METADATA_READER_NAME = "io.sentry.android.core.ManifestMetadataReader"

val CLASS_MODULES: Map<String, Set<ModuleIdentifier>> =
sortedMapOf(
Expand Down

This file was deleted.

Loading
Loading